Decode JSON into Elm Maybe

Brian Hicks has a series of posts on JSON decoders, you probably want to specifically look at Adding New Fields to Your JSON Decoder (which handles the scenario where you may or may not receive a field from a JSON object).

To start with, you'll probably want to use the elm-decode-pipeline package. You can then use the optional function to declare that your desc field may not be there. As Brian points out in the article, you can use the maybe decoder from the core Json.Decode package, but it will produce Nothing for any failure, not just being null. There is a nullable decoder, which you could also consider using, if you don't want to use the pipeline module.

Your decoder could look something like this:

modelDecoder : Decoder Model
modelDecoder =
    decode Model
        |> required "id" int
        |> required "name" string
        |> optional "desc" ( Just string) Nothing

Here's a live example on Ellie.

So if you're looking for a zero-dependency solution that doesn't require Json.Decode.Pipeline.

import Json.Decode as Decode exposing (Decoder)

modelDecoder : Decoder Model
modelDecoder =
    Decode.map3 Model
        (Decode.field "id"
        (Decode.field "name" Decode.string)
        (Decode.maybe (Decode.field "desc" Decode.string))

If you want to do this using the Model constructor as an applicative functor (because you'd need more 8 items).

import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Extra as Decode

modelDecoder : Decoder Model
modelDecoder =
    Decode.succeed Model
        |> Decode.andMap (Decode.field "id"
        |> Decode.andMap (Decode.field "name" Decode.string)
        |> Decode.andMap (Decode.maybe (Decode.field "desc" Decode.string))

Both of which can be used with Lists with Decode.list modelDecoder. I wish the applicative functions were in the standard library, but you'll have to reach into all of the *-extra libraries to get these features. Knowing how applicative functors work will help you understand more down the line, so I'd suggest reading about them. The Decode Pipeline solution abstracts this simple concept, but when you run into the need for Result.andMap or any other of the andMaps because there's not a mapN for your module or a DSL, you'll know how to get to your solution.

Because of the applicative nature of decoders, all fields should be able to be processed asynchronously and in parallel with a small performance gain, instead of synchronously like andThen, and this applies to every place that you use andMap over andThen. That said, when debugging switching to andThen can give you a place to give yourself an usable error per field that can be changed to andMap when you know everything works again.

Under the hood, JSON.Decode.Pipeline uses Json.Decode.map2 (which is andMap), so there's no performance difference, but uses a DSL that's negligibly more "friendly".

Brian Hicks' "Adding New Fields to Your JSON Decoder" post helped me develop the following. For a working example, see Ellie

import Html exposing (..)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline as JP
import String

type alias Item =
    { id : Int
    , name : String
    , desc : Maybe String

main =
    Decode.decodeString (Decode.list itemDecoder) payload
        |> toString
        |> String.append "JSON "
        |> text

itemDecoder : Decoder Item
itemDecoder =
    JP.decode Item
        |> JP.required "id"
        |> JP.required "name" Decode.string
        |> JP.optional "desc" ( Just Decode.string) Nothing