Search code examples
jsonelm

Make Json.Decode case insensitive in elm


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


Solution

  • 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 }"""