Make Json.Decode case insensitive in elm

Viewed 293

Is there an easy way to make Json.Decode case insensitive in elm (0.18)?

decodeDepartmentDate : Json.Decode.Decoder DepartmentDate
decodeDepartmentDate =
    Json.Decode.map6 DepartmentDate
        (field "nameOfDay" Json.Decode.string) 
        (field "orderDate" Convert.datePart)        
        (field "mealTimeID" Json.Decode.string)        
        (field "mealTime" Json.Decode.string)        
        (field "departmentID" Json.Decode.string)        
        (field "department" Json.Decode.string)        

I want to be able to use the same elm SPA against multiple back ends and avoid issues like this by default:

BadPayload "Expecting an object with a field named `nameOfDay` at _[11] 
but instead got: {\"NameOfDay\":\"Wednesday\",\"OrderDate\":\"2018-09-05T00:00:00\",
\"MealTimeID\":\"546ccee0-e070-403e-a15b-63f4e1366054\",\"MealTime\":\"All Day\",
\"StartTime\":\"2018/06/05 05:04:38\",\"DepartmentID\":\"066a1c9f-97da-487e-b82f-f933b159c042\",
\"Department\":\"Side walk\"}"

Thanks

2 Answers

As far as I'm aware, there's no ready-made solution for doing so. But you can make your own!

The easiest way is probably to just generate the different casings and make your own field decoder using oneOf:

myField name decoder =
    Decode.oneOf
        [ Decode.field name decoder
        , Decode.field (String.toLower) decoder
        ]

Another approach would be to decode the object as key/value pairs without decoding the values, transforming the keys and then re-encoding it to be able to use the existing JSON decoders on it:

lowerCaseKeys =
    Decode.keyValuePairs Decode.value
    |> Decode.map (List.map (\(key, value) -> (String.toLower key, value)))
    |> Decode.map (Encode.object)

But since the value is now wrapped in a Decoder you'd have to use decodeValue on that and ultimately end up with a double-wrapped Result, which isn't very nice. I might be missing some elegant way of making this work though.

Instead it seems better to not re-encode it, but just make your own field decoder to work on the dict. This will also allow you to ignore casing on the keys you specify.

lowerCaseKeys : Decode.Decoder (Dict.Dict String Decode.Value)
lowerCaseKeys =
    Decode.keyValuePairs Decode.value
        |> Decode.map (List.map (\( key, value ) -> ( String.toLower key, value )))
        |> Decode.map Dict.fromList

myField : String -> Decode.Decoder a -> Dict.Dict String Decode.Value -> Decode.Decoder a
myField name decode dict =
    case Dict.get (String.toLower name) dict of
        Just value ->
            case Decode.decodeValue decode value of
                Ok v ->
                    Decode.succeed v

                Err e ->
                    e |> Decode.errorToString |> Decode.fail

        Nothing ->
            Decode.fail "missing key"


result =
    Decode.decodeString (lowerCaseKeys |> Decode.andThen (myField "fOO" Decode.int)) """{ "Foo": 42 }"""

You can define a variant of field that disregards case.

fieldInsensitive : String -> Decode.Decoder a -> Decode.Decoder a
fieldInsensitive f d =
    let
       flow = String.toLower f
    in
    Decode.keyValuePairs Decode.value |> Decode.andThen 
      (\ l -> l |> List.filter (\(k, v) -> String.toLower k == flow)
                |> List.map (\(k, v) -> v)
                |> List.head
                |> Maybe.map Decode.succeed
                |> Maybe.withDefault (Decode.fail "field not found")
      ) |> Decode.andThen
      (\ v -> case Decode.decodeValue d v of
                  Ok w -> Decode.succeed w
                  Err e -> Decode.fail (Decode.errorToString e)
      )

This is more or less the same code as @glennsl's answer, but wrapped up in a self-contained function. The advantage is a simpler interface, the disadvantage is that if you lookup multiple fields in the same object you will be repeating work.

Note that this code makes a rather arbitrary decision if there are multiple fields with the same key up to case! For more reliable code, it might be a better idea to fail if a key exists more than once.

Related