The TsJson.Decode module is what you use for
By building a Decoder with this API, you're also describing the source of truth for how to take a TypeScript type and turn it into an Elm type. Note that there is complete type information here just by using this API (no magic parsing or static analysis needed).
Let's take this example:
import Json.Decode
int
|> map String.fromInt
|> runExample "1000"
--> { decoded = Ok "1000"
--> , tsType = "number"
--> }In this example, there are no differences from how we would write this with the elm/json API. Let's consider the type
information from two different sides: the Elm types (which would be used in an Elm type annotation), and the TypeScript
types (which show up in the TypeScript Declaration file that elm-ts-interop generates to gives you nice autocompletion and type information).
Decoder, TsJson.Decode.int, has the Elm type TsJson.Decode.Decoder Int. So Elm knows
this Decoder will either fail, or give us an Int.TsJson.Decode.map String.fromInt, the Elm type information changes. We're mapping with
String.fromInt : Int -> String. So that means we'll now decode into an Elm String instead of an Int. And that's
the final Elm type we'll end up with.TsJson.Decode.int expects a number from TypeScript.TsJson.Decode.map applies a function to the decoded Elm value, but it doesn't change what we expect from TypeScript.
So we still expect a number from TypeScript. And we're done, so that's the final type we expect to receive from TypeScript.We can apply more Decoders, like TsJson.Decode.list, for example, to expect an array of that type from TypeScript,
and a List of that Elm type. From there, it's just the same concept. All the type information about the type that Elm will decode into, and the type that Elm expects from TypeScript, is built up as you build a Decoder.
That means that the source of truth is the Decoder itself. Note that the Decoder doesn't specify just the Elm format, or just the TypeScript type as the source of truth. It specifies how to turn a TypeScript type into an Elm type as the source of truth. That means that
elm-ts-interop CLI at the appropriate time)import Json.Decode
succeed "abcdefg"
|> runExample "12345"
--> { decoded = Ok "abcdefg"
--> , tsType = "JsonValue"
--> }import Json.Decode
fail "Failure message"
|> runExample "123.45"
--> { decoded = Err "Problem with the given value:\n\n123.45\n\nFailure message"
--> , tsType = "JsonValue"
--> }import Json.Decode
bool
|> runExample "true"
--> { decoded = Ok True
--> , tsType = "boolean"
--> }import Json.Decode
float
|> runExample "1.23"
--> { decoded = Ok 1.23
--> , tsType = "number"
--> }import Json.Decode
int
|> runExample "1000"
--> { decoded = Ok 1000
--> , tsType = "number"
--> }Floating point values will cause a decoding error.
int
|> runExample "1.23"
--> { decoded = Err "Problem with the given value:\n\n1.23\n\nExpecting an INT"
--> , tsType = "number"
--> }import Json.Decode
string
|> runExample """ "Hello!" """
--> { decoded = Ok "Hello!"
--> , tsType = "string"
--> }import Json.Decode
field "first" string
|> runExample """{"first":"James","middle":"Tiberius","last":"Kirk"}"""
--> { decoded = Ok "James"
--> , tsType = "{ first : string }"
--> }import Json.Decode
import Json.Encode
type Mode
= DarkMode
| LightMode
modeDecoder : Decoder Mode
modeDecoder =
oneOf [ literal DarkMode <| Json.Encode.string "dark", literal LightMode <| Json.Encode.string "light" ]
|> (at [ "options", "mode" ])
modeDecoder
|> runExample """{
"options": { "mode": "dark" },
"version": "1.2.3"
}"""
--> { decoded = Ok DarkMode
--> , tsType = """{ options : { mode : "dark" | "light" } }"""
--> }import Json.Decode
list int
|> runExample "[1,2,3]"
--> { decoded = Ok [ 1, 2, 3 ]
--> , tsType = "number[]"
--> }Exactly the same as the list Decoder except that it wraps the decoded List into an Elm Array.
import Json.Decode
nullable int |> runExample "13"
--> { decoded = Ok (Just 13)
--> , tsType = "number | null"
--> }
nullable int |> runExample "null"
--> { decoded = Ok Nothing
--> , tsType = "number | null"
--> }
nullable int |> runExample "true"
--> { decoded = Err "Json.Decode.oneOf failed in the following 2 ways:\n\n\n\n(1) Problem with the given value:\n \n true\n \n Expecting null\n\n\n\n(2) Problem with the given value:\n \n true\n \n Expecting an INT"
--> , tsType = "number | null"
--> }You can express quite a bit with oneOf! The resulting TypeScript types will be a Union of all the TypeScript types
for each Decoder in the List.
import Json.Decode
import Json.Encode
list ( oneOf [ int |> map toFloat, float ] )
|> runExample "[1, 2, 3.14159, 4]"
--> { decoded = Ok [1.0, 2.0, 3.14159, 4.0]
--> , tsType = """(number | number)[]"""
--> }import Json.Decode
import Json.Encode
import Dict exposing (Dict)
dict int
|> runExample """{"alice":42,"bob":99}"""
--> { decoded = Ok (Dict.fromList [ ( "alice", 42 ), ( "bob", 99 ) ])
--> , tsType = "{ [key: string]: number }"
--> }import Json.Decode
keyValuePairs int
|> runExample """{ "alice": 42, "bob": 99 }"""
--> { decoded = Ok [ ( "alice", 42 ), ( "bob", 99 ) ]
--> , tsType = "{ [key: string]: number }"
--> }import Json.Decode
import Json.Encode
oneOrMore (::) int
|> runExample "[12345]"
--> { decoded = Ok [ 12345 ]
--> , tsType = """[ number, ...(number)[] ]"""
--> }
type TestResult
= Pass
| Fail String
testCaseDecoder : Decoder TestResult
testCaseDecoder =
oneOf [
field "tag" (literal Pass (Json.Encode.string "pass"))
, map2 (\() message -> Fail message)
( field "tag" (literal () (Json.Encode.string "fail")) )
( field "message" string )
]
oneOrMore (::) testCaseDecoder
|> runExample """[ { "tag": "pass" } ]"""
--> { decoded = Ok [ Pass ]
--> , tsType = """[ { tag : "pass" } | { message : string; tag : "fail" }, ...({ tag : "pass" } | { message : string; tag : "fail" })[] ]"""
--> }This is a safer (and more explicit) way to deal with optional fields compared to maybe. It may seem that wrapping
a field Decoder in a maybe Decoder achieves the same behavior, but the key difference is that the maybe version
will convert a failing decoder on a value that is present into Nothing, as if it wasn't present. Often what you want
is for the malformed version to fail, which is exactly what this function will do.
import Json.Decode
json : String
json = """{ "name": "tom", "age": null }"""
optionalField "height" float
|> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ height? : number }"
--> }
optionalField "age" int
|> runExample json
--> { decoded = Err "Problem with the value at json.age:\n\n null\n\nExpecting an INT"
--> , tsType = "{ age? : number }"
--> }import Json.Decode
json : String
json = """{ "name": "tom", "age": null }"""
optionalNullableField "height" float
|> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ height? : number | null }"
--> }
optionalNullableField "age" int
|> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ age? : number | null }"
--> }import Json.Decode
index 1 int
|> runExample "[0,100,200]"
--> { decoded = Ok 100
--> , tsType = "[JsonValue,number,...JsonValue[]]"
--> }
map2 Tuple.pair
( index 1 int )
( index 3 string )
|> runExample """[0,100,"a","b"]"""
--> { decoded = Ok ( 100, "b" )
--> , tsType = "[JsonValue,number,JsonValue,string,...JsonValue[]]"
--> }import Json.Decode
tuple string int
|> runExample """["abc", 123]"""
--> { decoded = Ok ( "abc", 123 )
--> , tsType = "[ string, number ]"
--> }import Json.Decode
triple string int bool
|> runExample """["abc", 123, true]"""
--> { decoded = Ok ( "abc", 123, True )
--> , tsType = "[ string, number, boolean ]"
--> }TypeScript allows you to define a tuple that can have any length of items with specific types.
TypeScript tuples are much like an Elm tuples, except two key differences:
import Json.Decode
startTuple (\a b c d -> { a = a, b = b, c = c, d = d })
|> element string
|> element int
|> element bool
|> element string
|> buildTuple
|> runExample """["abc", 123, true, "xyz"]"""
--> { decoded = Ok { a = "abc", b = 123, c = True, d = "xyz" }
--> , tsType = "[ string, number, boolean, string ]"
--> }import Json.Decode
int
|> map String.fromInt
|> runExample "1000"
--> { decoded = Ok "1000"
--> , tsType = "number"
--> }You can use map2, map3, etc. to build up decoders that have somewhat clearer error messages if something goes wrong.
Some people prefer the pipeline style using andMap as it has fewer parentheses and you don't have to change the number when
you add a new field. It's a matter of personal preference.
import Json.Decode
type alias Country = { name : String, populationInMillions : Int }
map2 Country
(field "name" string)
(field "population" (int |> map (\totalPopulation -> floor (toFloat totalPopulation / 1000000.0))))
|> runExample """ {"name": "Norway", "population":5328000} """
--> { decoded = Ok { name = "Norway", populationInMillions = 5 }
--> , tsType = "{ name : string; population : number }"
--> }This is useful for building up a decoder with multiple fields in a pipeline style. See https://github.com/elm-community/json-extra/blob/2.0.0/docs/andMap.md.
import Json.Decode
type alias Country = { name : String, populationInMillions : Int }
succeed Country
|> andMap (field "name" string)
|> andMap (field "population" (int |> map (\totalPopulation -> floor (toFloat totalPopulation / 1000000.0))))
|> runExample """ {"name": "Norway", "population":5328000} """
--> { decoded = Ok { name = "Norway", populationInMillions = 5 }
--> , tsType = "{ name : string; population : number }"
--> }TypeScript has support for literals.
import Json.Decode
import Json.Encode as JE
literal () (JE.string "unit")
|> runExample """ "unit" """
--> { decoded = Ok ()
--> , tsType = "\"unit\""
--> }import Json.Decode
null False
|> runExample "null"
--> { decoded = Ok False
--> , tsType = "null"
--> }import Json.Decode
example : AndThenContinuation (Int -> Decoder String)
example =
andThenInit
(\v1Decoder v2PlusDecoder version ->
case version of
1 -> v1Decoder
_ -> v2PlusDecoder
)
|> andThenDecoder (field "payload" string)
|> andThenDecoder (at [ "data", "payload" ] string)
field "version" int |> andThen example
|> runExample """{"version": 1, "payload": "Hello"}"""
--> { decoded = Ok "Hello"
--> , tsType = "({ version : number } & ({ data : { payload : string } } | { payload : string }))"
--> }This type allows you to combine all the possible Decoders you could run in an andThen continuation.
This API allows you to define all possible Decoders you might use up front, so that all possible TypeScript types the continuation could decode are known after building up the decoder instead of after running the decoder.
If you have an existing decoder, you can use it with an unknown type in TypeScript.
You can also decode an arbitrary JSON value as with elm/json, and then use elm/json to process it further.
Gives you an escape hatch to decode to a plain elm/json Json.Decode.Value. This has the TypeScript type unknown.
Avoid using this when possible.
import Json.Decode
value
|> runExample "Hello"
--> { decoded = (Json.Decode.decodeString Json.Decode.value "Hello" |> Result.mapError Json.Decode.errorToString)
--> , tsType = "JsonValue"
--> }If you need to run a regular JSON Decoder in an andThen, you can use this function, but it will yield a Decoder
with an unknown TypeScript type.
import Json.Decode as JD
field "version" int |> unknownAndThen (\versionNumber ->
if versionNumber == 1 then
JD.field "payload" JD.string
else
JD.at [ "data", "payload" ] JD.string
)
|> runExample """{"version": 1, "payload": "Hello"}"""
--> { decoded = Ok "Hello"
--> , tsType = "JsonValue"
--> }This function is somewhat risky in that it could cover up a failing Decoder by turning it into a Nothing. In
some cases, this may be what you're looking for, but if you're trying to deal with optional fields, it's safer to use
optionalField and it will give you better type information. See the thisShouldBeABoolean example below. In that example,
we're decoding a JSON value which should have been a boolean but instead is a string. We'd like the Decoder to fail
to let us know it wasn't able to process the value correctly, but instead it covers up the failure and decodes to Nothing.
So use this Decoder with care!
import Json.Decode
json : String
json = """{ "name": "tom", "age": 42, "thisShouldBeABoolean": "true" }"""
-- WARNING: uh oh, this may not be the desired behavior!
maybe (field "thisShouldBeABoolean" bool)
|> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ thisShouldBeABoolean : boolean } | JsonValue"
--> }
maybe (field "height" float)
|> runExample json
--> { decoded = Ok Nothing
--> , tsType = "{ height : number } | JsonValue"
--> }
field "height" (maybe float)
|> runExample json
--> { decoded = Err "Problem with the given value:\n\n{\n \"name\": \"tom\",\n \"age\": 42,\n \"thisShouldBeABoolean\": \"true\"\n }\n\nExpecting an OBJECT with a field named `height`"
--> , tsType = "{ height : number | JsonValue }"
--> }Usually you don't need to use these functions directly, but instead the code generated by the elm-ts-interop command line
tool will use these for you under the hood. These can be helpful for debugging, or for building new tools on top of this package.
Get a regular JSON Decoder that you can run using the elm/json API.
Just like a
Decoderinelm/json, except theseDecoders track the TypeScript types that they can successfully handle.