dillonkearns / elm-ts-json / TsJson.Decode

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).

The Elm type information

The TypeScript type information

Summary

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

Decoders


type alias Decoder value =
TsJson.Internal.Decode.Decoder value

Just like a Decoder in elm/json, except these Decoders 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"
--> }

Built-Ins

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

Objects

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

Composite Types

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

Arbitrary-Length Tuples

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:


type TupleBuilder value

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

Transformations

map : (value -> mapped) -> Decoder value -> Decoder mapped

import Json.Decode


int
    |> map String.fromInt
    |> runExample "1000"
--> { decoded = Ok "1000"
--> , tsType = "number"
--> }

Combining

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

TypeScript Literals

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

Discriminated Unions

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

Continuation

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


type AndThenContinuation a

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

elm/json Decoder Escape Hatches

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

Using Decoders

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

mapN for Combining More Than Two Items

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

Internals

runExample : String -> Decoder value -> { decoded : Result String value, tsType : String }