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)TsJson.Internal.Decode.Decoder value
Just like a Decoder
in elm/json
, except these Decoder
s track the TypeScript types that they can successfully handle.
succeed : value -> Decoder value
import Json.Decode
succeed "abcdefg"
|> runExample "12345"
--> { decoded = Ok "abcdefg"
--> , tsType = "JsonValue"
--> }
fail : String -> Decoder value
import Json.Decode
fail "Failure message"
|> runExample "123.45"
--> { decoded = Err "Problem with the given value:\n\n123.45\n\nFailure message"
--> , tsType = "JsonValue"
--> }
bool : Decoder Basics.Bool
import Json.Decode
bool
|> runExample "true"
--> { decoded = Ok True
--> , tsType = "boolean"
--> }
float : Decoder Basics.Float
import Json.Decode
float
|> runExample "1.23"
--> { decoded = Ok 1.23
--> , tsType = "number"
--> }
int : Decoder Basics.Int
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"
--> }
string : Decoder String
import Json.Decode
string
|> runExample """ "Hello!" """
--> { decoded = Ok "Hello!"
--> , tsType = "string"
--> }
field : String -> Decoder value -> Decoder value
import Json.Decode
field "first" string
|> runExample """{"first":"James","middle":"Tiberius","last":"Kirk"}"""
--> { decoded = Ok "James"
--> , tsType = "{ first : string }"
--> }
at : List String -> Decoder value -> Decoder value
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" } }"""
--> }
list : Decoder value -> Decoder (List value)
import Json.Decode
list int
|> runExample "[1,2,3]"
--> { decoded = Ok [ 1, 2, 3 ]
--> , tsType = "number[]"
--> }
array : Decoder value -> Decoder (Array value)
Exactly the same as the list
Decoder
except that it wraps the decoded List
into an Elm Array
.
nullable : Decoder value -> Decoder (Maybe value)
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"
--> }
oneOf : List (Decoder value) -> Decoder value
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)[]"""
--> }
dict : Decoder value -> Decoder (Dict String value)
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 }"
--> }
keyValuePairs : Decoder value -> Decoder (List ( String, value ))
import Json.Decode
keyValuePairs int
|> runExample """{ "alice": 42, "bob": 99 }"""
--> { decoded = Ok [ ( "alice", 42 ), ( "bob", 99 ) ]
--> , tsType = "{ [key: string]: number }"
--> }
oneOrMore : (a -> List a -> value) -> Decoder a -> Decoder value
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" })[] ]"""
--> }
optionalField : String -> Decoder value -> Decoder (Maybe value)
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 }"
--> }
optionalNullableField : String -> Decoder value -> Decoder (Maybe value)
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 }"
--> }
index : Basics.Int -> Decoder value -> Decoder value
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[]]"
--> }
tuple : Decoder value1 -> Decoder value2 -> Decoder ( value1, value2 )
import Json.Decode
tuple string int
|> runExample """["abc", 123]"""
--> { decoded = Ok ( "abc", 123 )
--> , tsType = "[ string, number ]"
--> }
triple : Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder ( value1, value2, value3 )
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:
startTuple : a -> TupleBuilder a
buildTuple : TupleBuilder a -> Decoder a
element : Decoder a -> TupleBuilder (a -> b) -> TupleBuilder b
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 ]"
--> }
map : (value -> mapped) -> Decoder value -> Decoder mapped
import Json.Decode
int
|> map String.fromInt
|> runExample "1000"
--> { decoded = Ok "1000"
--> , tsType = "number"
--> }
map2 : (value1 -> value2 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder mapped
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 }"
--> }
andMap : Decoder a -> Decoder (a -> b) -> Decoder b
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 }"
--> }
literal : value -> Json.Encode.Value -> Decoder value
TypeScript has support for literals.
import Json.Decode
import Json.Encode as JE
literal () (JE.string "unit")
|> runExample """ "unit" """
--> { decoded = Ok ()
--> , tsType = "\"unit\""
--> }
null : value -> Decoder value
import Json.Decode
null False
|> runExample "null"
--> { decoded = Ok False
--> , tsType = "null"
--> }
stringLiteral : value -> String -> Decoder value
A convenience function for building literal (Json.Encode.string "my-literal-string")
.
import TsJson.Decode as TsDecode
TsDecode.stringLiteral () "unit"
|> TsDecode.runExample """ "unit" """
--> { decoded = Ok ()
--> , tsType = "\"unit\""
--> }
stringUnion : List ( String, value ) -> Decoder value
A convenience function for building a union out of string literals.
import TsJson.Decode as TsDecode
type Severity
= Info
| Warning
| Error
TsDecode.stringUnion
[ ( "info", Info )
, ( "warning", Warning )
, ( "error", Error )
]
|> TsDecode.runExample """ "info" """
--> { decoded = Ok Info
--> , tsType = "\"info\" | \"warning\" | \"error\""
--> }
discriminatedUnion : String -> List ( String, Decoder decoded ) -> Decoder decoded
Decode a TypeScript Discriminated Union with a String discriminant value. For example, if you wanted to decode something with the following TypeScript type:
{ id : number; role : "admin" } | { role : "guest" }
You could use this Decoder:
import TsJson.Decode as TsDecode
type User
= Admin { id : Int }
| Guest
TsDecode.discriminatedUnion "role"
[ ( "admin"
, TsDecode.succeed (\id -> Admin { id = id })
|> TsDecode.andMap (TsDecode.field "id" TsDecode.int)
)
, ( "guest", TsDecode.succeed Guest )
]
|> TsDecode.runExample """{"role": "admin", "id": 123}"""
--> { decoded = Ok (Admin { id = 123 })
--> , tsType = """{ id : number; role : "admin" } | { role : "guest" }"""
--> }
andThen : AndThenContinuation (value -> Decoder decodesTo) -> Decoder value -> Decoder decodesTo
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.
andThenInit : a -> AndThenContinuation a
andThenDecoder : Decoder value -> AndThenContinuation (Decoder value -> final) -> AndThenContinuation final
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.
value : Decoder Json.Decode.Value
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"
--> }
unknownAndThen : (a -> Json.Decode.Decoder b) -> Decoder a -> Decoder b
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"
--> }
maybe : Decoder value -> Decoder (Maybe value)
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.
decoder : Decoder value -> Json.Decode.Decoder value
Get a regular JSON Decoder that you can run using the elm/json
API.
tsType : Decoder value -> Internal.TsJsonType.TsType
map3 : (value1 -> value2 -> value3 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder mapped
map4 : (value1 -> value2 -> value3 -> value4 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder value4 -> Decoder mapped
map5 : (value1 -> value2 -> value3 -> value4 -> value5 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder value4 -> Decoder value5 -> Decoder mapped
map6 : (value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder value4 -> Decoder value5 -> Decoder value6 -> Decoder mapped
map7 : (value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder value4 -> Decoder value5 -> Decoder value6 -> Decoder value7 -> Decoder mapped
map8 : (value1 -> value2 -> value3 -> value4 -> value5 -> value6 -> value7 -> value8 -> mapped) -> Decoder value1 -> Decoder value2 -> Decoder value3 -> Decoder value4 -> Decoder value5 -> Decoder value6 -> Decoder value7 -> Decoder value8 -> Decoder mapped
runExample : String -> Decoder value -> { decoded : Result String value, tsType : String }