16

Given the following JSON:

[
  {
    "id": 0,
    "name": "Item 1",
    "desc": "The first item"
  },
  {
    "id": 1,
    "name": "Item 2"
  }
]

How do you decode that into the following Model:

type alias Model =
    { id : Int
    , name : String
    , desc : Maybe String
    }
Matthew Rankin
  • 457,139
  • 39
  • 126
  • 163

3 Answers3

25

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" (Json.map Just string) Nothing

Here's a live example on Ellie.

rofrol
  • 14,438
  • 7
  • 79
  • 77
bdukes
  • 152,002
  • 23
  • 148
  • 175
  • After posting my question, further Googling led me to Brian Hicks' post on JSON decoders. I was pleased to see your answer when I came back. Glad to have found a good source, that's helpful to others as well. – Matthew Rankin Feb 17 '17 at 22:03
  • In your last line, I think you want `(Json.map Just string)` as opposed to `Json.map Just int)`. – Matthew Rankin Feb 17 '17 at 22:04
  • Thanks, I've updated it with `string` and an Ellie example. Hope it helps! – bdukes Feb 17 '17 at 22:12
  • Yes! I came to same answer based on Brian Hicks' info. I'm marking your answer as accepted, since you posted first. – Matthew Rankin Feb 17 '17 at 22:19
  • 1
    As of Elm 0.19 the package has been renamed to to [elm-json-decode-pipeline](https://package.elm-lang.org/packages/NoRedInk/elm-json-decode-pipeline) – Yoni Gibbs Sep 11 '19 at 09:18
18

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.int)
        (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.int)
        |> 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".

toastal
  • 1,056
  • 8
  • 16
  • 1
    This should probably be the chosen response. Theres no need to pull in a completely separate dependency just because you have this behavior in your API. Its supported out of the box. – jnmandal Feb 22 '18 at 15:53
  • On advantage of the `andThen` route is that you have the opportunity to bind out the a specific error message, but that's mostly useful when debugging. `andMap` should technically be parallelizable because it's applicative. – toastal Mar 06 '18 at 02:04
2

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" Decode.int
        |> JP.required "name" Decode.string
        |> JP.optional "desc" (Decode.map Just Decode.string) Nothing
Matthew Rankin
  • 457,139
  • 39
  • 126
  • 163