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
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
In Elm, there's no native way to encode/decode a custom type. This makes send and receive custom-typed values to JS difficult. I was confused for a while and wonder how to handle simple custom types like the one below?
type MyCustomType
= A
| B
| C
Here's a quick and simple demo of how to encode and decode any custom types.
Say you have a custom type like this:
type MyCustomType
= A
| B
| C
You can encode MyCustomType directly as a string:
encodeMyCustomType : MyCustomType -> Encode.Value
encodeMyCustomType myCustomType =
Encode.string <|
case myCustomType of
A -> "A"
B -> "B"
C -> "C"
Decoding MyCustomType is slightly more involved. You need to use Decode.andThen to check which variant is found and use Decode.fail in case no valid variant is found:
Decode.string |>
Decode.andThen
(\str ->
case str of
"A" -> Decode.succeed A
"B" -> Decode.succeed B
"C" -> Decode.succeed C
_ -> Decode.fail "Invalid MyCustomType"
)
I need to consume a json source that represents floats as strings* and I can't figure out how.
It is almost easy:
Json.Decode.map String.toFloat Json.Decode.string
However, that produces a Maybe Float and I'd prefer it fail altogether if it cant decode the string.
(*) The reason for this is that the real datatype is Decimal, so "1.5" != "1.50". My application doesn't have to care though.
You can either install elm-community/json-extra and use Json.Decode.Extra.parseFloat
or just copy its implementation
fromMaybe : String -> Maybe a -> Decode.Decoder a
fromMaybe error val =
case val of
Just v ->
Decode.succeed v
Nothing ->
Decode.fail error
parseFloat : Decode.Decoder Float
parseFloat =
Decode.string |> Decode.andThen (String.toFloat >> fromMaybe "failed to parse as float")
Another option which makes fromMaybe unnecessary:
floatDecoder : Json.Decoder Float
floatDecoder =
Json.string |> Json.andThen (String.toFloat >> Maybe.withDefault 0.0 >> Json.succeed)
Just in case it helps someone else ;)
I have a basic append function
let append item list = item :: list
And i have a' list option and option Some("something")
let listOption = Some []
I want to add the value "something" to listOption. How can I do it without using pattern matching and Option.get but by lifting append function?
Any help would be appreciated
You can use maybe computation expression
type MaybeBuilder() =
member this.Bind(m, f) = Option.bind f m
member this.Return(x) = Some x
let maybe = new MaybeBuilder()
let append item list = item :: list
let appendLifted item list =
maybe {
let! l = list
let! i = item
return append i l
}
[<EntryPoint>]
let main argv =
appendLifted (Some "abc") (Some [])
0
It looks like a home work...
If you want to add a value (not an option) at the head of a list option, you can simply do this which will return None if the list option is None:
let liftedAppend item optList =
optList |> Option.bind (fun list -> Some (item :: list))
liftedAppend signature is:
'a -> 'a list option -> 'a list option
But talking about lifting stricto sensu, as the signature of your append function is:
'a -> 'a list -> 'a list
the signature of the lifted function should be:
'a option -> 'a list option -> 'a list option
That means the first argument have to be an option and I guess you want to check if it's Some or None. If so attentively read other's replies.
You can use something like this, which the de-sugared Lanayx's computation expression.
let liftedAppend optItem optList =
optList |> Option.bind (fun list ->
optItem |> Option.bind (fun item -> Some (item :: list)))
This works:
listOption
|> Option.map (append 11)
|> printfn "%A" // Some [11]
but to create a lifted append:
let liftedAppend v = Option.map (append v)
listOption
|> liftedAppend 11
|> printfn "%A" // Some [11]
The signature of the functions are:
val append : 'a -> 'a list > 'a list
val liftedAppend: 'a -> 'a list option -> 'a list option
To pass both parameters as options you can use Option.map2:
let liftedAppend2 vO = vO |> Option.map2 append
listOption
|> liftedAppend2 (Some 11)
|> printfn "%A" // Some [11]
Which has signature:
val liftedAppend2: a option -> 'a list option -> 'a list option
My JSON looks like the following
{ "resp":
[ [1, "things"]
, [2, "more things"]
, [3, "even more things"]
]
}
the problem is that I can't parse the JSON tuples into Elm tuples:
decodeThings : Decoder (List (Int, String))
decodeThings = field "resp" <| list <| map2 (,) int string
It compiles, but when ran, it throws
BadPayload "Expecting an Int at _.resp[2] but instead got [3, \"even more things\"]
For some reason it reads [3, "even more things"] as only one thing and not as tuple in JSON format.
How can I parse my JSON into a List (Int, String)?
The accepted answer is more complicated than it needs to be. Try:
import Json.Decode as Decode
decodeTuple : Decode.Decoder (Int, String)
decodeTuple =
Decode.map2 Tuple.pair
(Decode.index 0 Decode.int)
(Decode.index 1 Decode.string)
and then, as you note, for the list
Decode.list decodeTuple
You need a decoder which turns a javascript array of size two into an Elm tuple of size two. Here is an example decoder:
arrayAsTuple2 : Decoder a -> Decoder b -> Decoder (a, b)
arrayAsTuple2 a b =
index 0 a
|> andThen (\aVal -> index 1 b
|> andThen (\bVal -> Json.Decode.succeed (aVal, bVal)))
You can then amend your original example as follows:
decodeThings : Decoder (List (Int, String))
decodeThings = field "resp" <| list <| arrayAsTuple2 int string
(Note that my example decoder does not fail if there are more than two elements, but it should get you pointed in the right direction)
I could not get either Chad Gilbert's or Simon H's solution to work with Elm 0.19. I am quite new to Elm, but this is what I could get to work:
import Json.Decode as Decode
import Json.Decode.Extra as Decode
{-| Decodes two fields into a tuple.
-}
decodeAsTuple2 : String -> Decode.Decoder a -> String -> Decode.Decoder b -> Decode.Decoder (a, b)
decodeAsTuple2 fieldA decoderA fieldB decoderB =
let
result : a -> b -> (a, b)
result valueA valueB =
(valueA, valueB)
in
Decode.succeed result
|> Decode.andMap (Decode.field fieldA decoderA)
|> Decode.andMap (Decode.field fieldB decoderB)
My question is simple. Why wrong pattern matching does not throw exception in Maybe monad. For clarity :
data Task = HTTPTask {
getParams :: [B.ByteString],
postParams :: [B.ByteString],
rawPostData :: B.ByteString
} deriving (Show)
tryConstuctHTTPTask :: B.ByteString -> Maybe Task
tryConstuctHTTPTask str = do
case decode str of
Left _ -> fail ""
Right (Object trie) -> do
Object getP <- DT.lookup (pack "getParams") trie
Object postP <- DT.lookup (pack "postParams") trie
String rawData <- DT.lookup (pack "rawPostData") trie
return $ HTTPTask [] [] rawData
Look at tryConstuctHTTPTask function. I think that when the pattern does not match (for example "Object getP") we must get something like "Prelude.Exception", instead i get the "Nothing". I like this behavior but i am not understand why.
Thanks.
Doing pattern <- expression in a do-block, will call fail when the pattern does not match. So it is equivalent to doing
expression >>= \x ->
case x of
pattern -> ...
_ -> fail
Since fail is defined as Nothing in the Maybe monad, you get Nothing for failed pattern matches using <-.