3

I’m stuck with a decoder that should decode an array [ 9.34958, 48.87733, 1000 ] to a Point, where index 2 (elevation) is optional.

type alias Point =
    { elev : Maybe Float
    , at : Float
    , lng : Float
    }

Therefore I created following decoder:

fromArrayDecoder : Decoder Point
fromArrayDecoder =
    map3 Point
        (index 2 Decode.float |> Decode.maybe)
        (index 1 Decode.float)
        (index 0 Decode.float)

My problem now is, that this decoder succeeds when index 2 is missing or is of any type like string etc. But I want it only to succeed if elev is missing, not if it has the wrong type. Is there any way of accomplishing this?

bdukes
  • 152,002
  • 23
  • 148
  • 175
benbro00002
  • 151
  • 4
  • What do you mean by "missing"? Is it `null`, or does the array just have two elements in that case? – glennsl Feb 05 '20 at 18:52

2 Answers2

5

If by "missing" you mean the value can be null, you can just use nullable instead of maybe:

fromArrayDecoder : Decoder Point
fromArrayDecoder =
  map3 Point
    (index 2 Decode.float |> Decode.nullable)
    (index 1 Decode.float)
    (index 0 Decode.float)

If it's either a 3-element array or a two-element array you can use oneOf to try several decoders in order:

fromTwoArrayDecoder : Decoder Point
fromTwoArrayDecoder =
  map3 (Point Nothing)
    (index 1 Decode.float)
    (index 0 Decode.float)

fromThreeArrayDecoder : Decoder Point
fromThreeArrayDecoder =
  map3 Point
    (index 2 Decode.float |> Decode.map Just)
    (index 1 Decode.float)
    (index 0 Decode.float)

fromArrayDecoder : Decoder Point
fromArrayDecoder =
  oneOf
    [ fromThreeArrayDecoder
    , fromTwoArrayDecoder
    ]

Just remember to try the 3-element decoder first, as the 2-element decoder will succeed on a 3-element array as well, but the opposite does not.

glennsl
  • 28,186
  • 12
  • 57
  • 75
3

I agree that the fact Json.Decode.maybe giving you Nothing on a wrong value rather than just a missing one is surprising.

elm-json-decode-pipeline can work in the way you want without getting too verbose.

> d = Decode.succeed Point 
|   |> optional "2" (Decode.float |> Decode.map Just) Nothing 
|   |> required "1" Decode.float 
|   |> required "0" Decode.float
|
> "[1, 2, \"3\"]" |> Decode.decodeString d
Err (Failure ("Json.Decode.oneOf failed in the following 2 ways:\n\n\n\n(1) Problem with the given value:\n    \n    \"3\"\n    \n    Expecting a FLOAT\n\n\n\n(2) Problem with the given value:\n    \n    \"3\"\n    \n    Expecting null") <internals>)
    : Result Decode.Error Point
> "[1, 2, 3]" |> Decode.decodeString d
Ok { at = 2, elev = Just 3, lng = 1 }
    : Result Decode.Error Point
> "[1, 2]" |> Decode.decodeString d
Ok { at = 2, elev = Nothing, lng = 1 }
    : Result Decode.Error Point

(You can see from the error that under the hood it is using oneOf like in glennsl's answer.)

The only potentially surprising thing here is that you need to pass strings rather than int indexes, as there isn't a specific version for lists but you can access list indexes as though they are field names. This does mean that this version is subtly different in that it will not throw an error if you can an object with number field names rather than an array, but I can't imagine that really being an issue. The more real issue is it could make your error messages less accurate:

> "[0]" |> Decode.decodeString (Decode.field "0" Decode.int)
Ok 0 : Result Decode.Error Int
> "[]" |> Decode.decodeString (Decode.field "0" Decode.int)
Err (Failure ("Expecting an OBJECT with a field named `0`") <internals>)
    : Result Decode.Error Int
> "[]" |> Decode.decodeString (Decode.index 0 Decode.int)
Err (Failure ("Expecting a LONGER array. Need index 0 but only see 0 entries") <internals>)

Note that you do still have to to avoid using Json.Decode.maybe. It may be tempting to write optional "2" (Decode.maybe Decode.float) Nothing which will result in the same behaviour as you originally got.

Gareth Latty
  • 86,389
  • 17
  • 178
  • 183