Skip to main content

Json.Decode

Turn JSON values into Rescript values

Primitives

t

type t<'a>

A value that knows how to decode JSON values.

value

type value = Js.Json.t

Represents a JavaScript value. Alias to Json.value;

string

let string: t<string>

Decode a JSON string into a Rescript string.

decodeString("true", string)                   == Error(...)
decodeString("42", string) == Error(...)
decodeString("3.14", string) == Error(...)
decodeString("\"hello\", string) == Ok("hello")
decodeString("{ \"hello\": 42 }", string) == Error(...)

bool

let bool: t<bool>

Decode a JSON bool into a Rescript bool.

decodeString("true", bool)                    == Ok(true)
decodeString("42", bool) == Error(...)
decodeString("3.14", bool) == Error(...)
decodeString("\"hello\", bool) == Error(...)
decodeString("{ \"hello\": 42 }", bool) == Error(...)

int

let int: t<int>

Decode a JSON int into a Rescript int.

decodeString("true", int)                  == Error(...)
decodeString("42", int) == Ok(42)
decodeString("3.14", int) == Error(...)
decodeString("\"hello\", int) == Error(...)
decodeString("{ \"hello\": 42 }", int) == Error(...)

float

let float: t<float>

Decode a JSON float into a Rescript float.

decodeString("true", float)                  == Error(...)
decodeString("42", float) == Error(...)
decodeString("3.14", float) == Ok(3.14)
decodeString("\"hello\", float) == Error(...)
decodeString("{ \"hello\": 42 }", float) == Error(...)

Data Structures

nullable

let nullable: t<'a> => t<option<'a>>

Decode a nullable JSON value into a Rescript value.

decodeString("13", nullable(int))   == Ok(Some(13))
decodeString("42", nullable(int)) == Ok(Some(42))
decodeString("null", nullable(int)) == Ok(None)
decodeString("true", nullable(int)) == Error(...)

array

let array: t<'a> => t<array<'a>> 

Decode a JSON value into a Rescript array.

decodeString("[1,2,3]", array(int))      == Ok([1, 2, 3])
decodeString("[true,false]", array(int)) == Ok([true, false])

list

let list: t<'a> => t<list<'a>> 

Decode a JSON value into a Rescript list.

decodeString("[1,2,3]", list(int))      == Ok(list{1, 2, 3})
decodeString("[true,false]", list(int)) == Ok(list{true, false})

dict

let dict: t<'a> => t<Js.Dict.<'a>> 

Decode a JSON value into a Rescript dict.

decodeString("{ \"alice\": 42, \"bob\": 99 }", dict(int))
== Ok(Js.Dict.fromArray([("alice", 42), ("bob", 99)]))

keyValuePairs

let keyValuePairs: t<'a> => t<array<(string, 'a)>>

Decode a JSON value into a Rescript [array] of pair.

decodeString("{ \"alice\": 42, \"bob\": 99 }", keyValuePairs(int))
== Ok([("alice", 42), ("bob", 99)])

tuple2

let tuple2: (t<'a>, t<'b>) => t<('a, 'b)>

Decode a JSON value into a Rescript tuple with 2 elements.

This is helpful if you're dealing with a heterogeneous JSON array, like [1, "hey"]. Modeling your JSON like this is generally a bad idea. Unfortunately, we sometimes have to use JSON/JavaScript from places outside our control, hence these functions.

decodeString("[1, \"hey\"]", tuple2(int, string))
== Ok((1, "hey"))

tuple3

let tuple3: (t<'a>, t<'b>, t<'c>) => t<('a, 'b, 'c)>

Decode a JSON value into a Rescript tuple with 3 elements.

decodeString("[1, 8.0, \"hey\"]", tuple3(int, float, string))
== Ok((1, 8., "hey"))

tuple4-8

For tuple4-tuple8, using it is just like the examples for tuple2 and tuple3 only with more elements & decoders.

let tuple4: (t<'a>, t<'b>, t<'c>, t<'d>) => t<('a, 'b, 'c, 'd)>
let tuple5: (t<'a>, t<'b>, t<'c>, t<'d>, t<'e>) => t<('a, 'b, 'c, 'd, 'e)>
let tuple6: (t<'a>, t<'b>, t<'c>, t<'d>, t<'e>, t<'f>) => t<('a, 'b, 'c, 'd, 'e, 'f)>
let tuple7: (t<'a>, t<'b>, t<'c>, t<'d>, t<'e>, t<'f>, t<'g>) => t<('a, 'b, 'c, 'd, 'e, 'f, 'g)>
let tuple8: (
t<'a>,
t<'b>,
t<'c>,
t<'d>,
t<'e>,
t<'f>,
t<'g>,
t<'h>,
) => t<('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h)>

Object Primatives

field

let field: (string, t<'a>) => t<'a>

Decode a JSON object, requiring a particular field.

decodeString("{ \"x\": 42 }",                 field("x", int))== Ok(42)
decodeString("{ \"x\": 42, \"y\": 50 }", field("x", int))== Ok(42)
decodeString("{ \"x\": true }", field("x", int))== Error(...)
decodeString("{ \"y\": 50 }", field("x", int))== Error(...)

The object can have other fields. Lots of them! The only thing this decoder cares about is if x is present and that the value there is an int.

Check out map2 to see how to decode multiple fields!

at

let at: (string, array<string>, t<'a>) => t<'a>

Decode a nested JSON object, requiring certain fields.

let json = "{ \"person\": { \"name\": \"Maria\", \"age\": 24 } }"

decodeString(json, at(["person", "name"], string)) == Ok("Maria")
decodeString(json, at(["person", "age" ], int )) == Ok(24)

This is really just a shorthand for saying things like:

field("person", field("name", string)) == at(["person", "name"], string)

index

let index: (int, t<'a>) => t<'a>

Decode a nested JSON object, requiring a certain index.

let json = "[ \"Maria\", \"Caleb\", \"Sofia\" ]"

decodeString(json, index(0, string)) == Ok("Maria")
decodeString(json, index(2, string)) == Ok("Sofia")

Inconsistent Structure

option

let option: t<'a> => t<option<'a>>

Helpful for dealing with optional fields. Here are a few slightly different examples:

let json = "{ \"name\": \"Maria\", \"age\": 24 }"

decodeString(json, option(field("age", int ))) == Ok(Some(24))
decodeString(json, option(field("name", int ))) == Ok(None)
decodeString(json, option(field("height", float))) == Ok(None)

decodeString(json, field("age", option(float))) == Ok(Some(24))
decodeString(json, field("name", option(float))) == Ok(None)
decodeString(json, field("height", option(float))) == Error(...)

Notice the last example! It is saying we must have a field named height and the content may be a float. There is no height field, so the decoder fails.

Point is, option will make exactly what it contains conditional. For optional fields, this means you probably want it outside a use of field or at.

oneOf

let oneOf: (t<'a>, array<t<'a>>) => t<'a>

Try a bunch of different decoders. This can be useful if the JSON may come in a couple different formats. For example, say you want to read an array of numbers, but some of them are [null].

let badIntDecoder: t<int> =
oneOf(
int, // the first decoder to try
[null(0)] // the other decoders to try
)

decodeString("[1,2,null,4]", badIntDecoder) == Ok([1, 2, 0, 4])

Why would someone generate JSON like this? Questions like this are not good for your health. Point is, that you can use oneOf to handle situations like this!

You could also use oneOf to help version your data. Try the latest format, then a few older ones that you still support. You could use andThen to be even more particular if you wanted.

Running Decoders

decodeString

let decodeString: (string, t<'a>) => result<'a, error>

Parse the given string into a JSON value and then run the decoder on it. This will fail if the string is not well-formed JSON or if the Decoder fails for some reason.

  decodeString(\"1\", int)     == Ok(1)
decodeString(\"1 + 2\", int) == Error(...)

decodeValue

let decodeValue: (value, t<'a>) => result<'a, error>

Run a decoder on some JSON value. This is helpful if you want to bring in some JavaScript value as a value, then run a decoder on it.

@module(\"my-func\")
external getUser: unit => value = \"default\"

let firstNameDecoder: t<string> =
oneOf(
field(\"firstName\", string),
[
field(\"first_name\", string),
field(\"first-name\", string),
]
)

let myFunc = () => {
getUser()
->decodeValue(firstNameDecoder)
}

error

type rec error =
| Failure(string, value)
| Index(int, error)
| Field(string, error)
| OneOf(error, array<error>)

A structured error describing exactly how the decoder failed. You can use this to create more elaborate visualizations of a decoder problem. For example, you could show the entire JSON object and show the part causing the failure in red.

errorToString

let errorToString: error => string

Convert a decoding error into a [string] that is nice for debugging. The output of this function produces a multi-line string.

For example, the following decoder

decodeString(`{ \"a\": { \"b\": \"hey\" } }`, at(\"a\", [\"b\"], int))

produces the string

Problem with the value at json["a"]["b"]:

"hey"

Expecting an INT

Combine

Note: If you run out of map functions, take a look at andMap} which makes it easier to handle large objects, but produces lower quality type errors.

map

let map: (t<'a>, ~f: 'a => 'b) => t<'b>

Transform a decoder. Maybe you just want to know the length of a string:

let stringLength: t<int> = map(string, ~f=Js.String.length)

It is often helpful to use map with oneOf, like when defining nullable:

let nullable = (decoder: t<'a>): t<option<'a>> =
oneOf(
null(None),
[map(decoder, ~f=val => Some(val))]
)

map2

let map2: (t<'a>, t<'b>, ~f: ('a, 'b) => 'c) => t<'c>

Try two decoders and then combine the result. We can use this to decode objects with many fields:

type point = { x: float, y: float }

let pointDecoder =
map2(
field("x", float),
field("y", float),
~f=(x, y) => { x: x, y: y })
)

let json = "{ \"x\": 2, \"y\": 5 }"
decodeString(json, pointDecoder) == Ok({ x: 2, y: 5 })

It tries each individual decoder and puts the result together with in the ~f function.

map3

let map3: (t<'a>, t<'b>, t<'c>, ~f: ('a, 'b, 'c) => 'val) => t<'val>

Try three decoders and then combine the result. We can use this to decode objects with many fields:

type person = { name: string, age: int, height: float }

let pointDecoder =
map3(
field("name", string),
at("info", ["age"], int),
at("info", ["height"], float),
~f=(name, age, height) => { name: name, age: age, height: height })
)

let json = "{ \"name\": "Maria", \"info\": { \"age\": 28, \"height\": 1.5 } }"
decodeString(json, pointDecoder)
== Ok({ name: "Maria", age: 28, height: 1.5 })

It tries each individual decoder and puts the result together with in the ~f function.

map4-8

For map4-map8, using it is just like the examples for map2 and map3 only with more elements & decoders.

let map4: (t<'a>, t<'b>, t<'c>, t<'d>, ~f: ('a, 'b, 'c, 'd) => 'val) => t<'val>
let map5: (t<'a>, t<'b>, t<'c>, t<'d>, t<'e>, ~f: ('a, 'b, 'c, 'd, 'e) => 'val) => t<'val>
let map6: (
t<'a>,
t<'b>,
t<'c>,
t<'d>,
t<'e>,
t<'f>,
~f: ('a, 'b, 'c, 'd, 'e, 'f) => 'val,
) => t<'val>
let map7: (
t<'a>,
t<'b>,
t<'c>,
t<'d>,
t<'e>,
t<'f>,
t<'g>,
~f: ('a, 'b, 'c, 'd, 'e, 'f, 'g) => 'val,
) => t<'val>
let map8: (
t<'a>,
t<'b>,
t<'c>,
t<'d>,
t<'e>,
t<'f>,
t<'g>,
t<'h>,
~f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h) => 'val,
) => t<'val>

Fancy

value

let value: t<value>

Do not do anything with a JSON value, just bring it into Rescript as a value. This can be useful if you have particularly complex data that you would like to deal with later. Or if you are going to send it out another external and do not care about its structure.

null

let null: 'a => t<'a>

Decode a JSON string into some Rescript value.

decodeString("null", null(false)) == Ok(false)
decodeString("null", null(42)) == Ok(42)
decodeString("42", null(42)) == Error(...)
decodeString("false", null(42)) == Error(...)

succeed

let succeed: 'a => t<'a>

Ignore the JSON and produce a certain Rescript value.

decodeString("true", succeed(42))    == Ok(42)
decodeString("[1,2,3]", succeed(42)) == Ok(42)
decodeString("hello", succeed(42)) == Error(...) // "hello" is not a valid JSON string, so this fails

fail

let fail: string => t<'a>

Ignore the JSON and make the decoder fail. This is handy when used with oneOf or andThen where you want to give a custom error message in some case.

See the andThen docs for an example.

andMap

let andMap: (t<'a => 'b>, t<'a>) => t<'b>

Chain decoders together and then transfrom them all together. This is very often going to be used with succeed.

If you want to define map3, you could do:

let map3: (t<'a>, t<'b>, t<'c>, ~f: ('a, 'b, 'c) => 'val) => t<'val> = (
decoderA,
decoderB,
decoderC,
~f,
) =>
succeed(f)
->andMap(decoderA)
->andMap(decoderB)
->andMap(decoderC)

You can use this function to chain together field decoders to construct a Rescript record from a JSON one:

type t = { id: int, name: string }

let decoder: Json.Decode.t<t> = {
open Json.Decode
succeed((id, name) => { id: id, name: name })
->andMap(field("id", int))
->andMap(field("name", string))

decodeString(`{ "id": 1, "name": "Marcos" }`, decoder) == { id: 1, name: "Marcos" }
}

This is style of decoding is sometimes called pipeline decoding!

andThen

let andThen: (t<'a>, ~f: 'a => t<'b>) => t<'b>

Create decoders that depend on previous results. If you are creating versioned data, you might do something like this:

let infoDecoder1: t<info> = x
let infoDecoder2: t<info> = y
let infoDecoder3: t<info> = z

let infoDecoder = (version: int): t<info> =
switch version {
| 1 => infoDecoder1
| 2 => infoDecoder2
| 3 => infoDecoder3
| _ => fail(`Trying to decode info, but version ${Belt.Int.toString(version)} is not supported`)
}

let info: t<info> =
field("version", int)
->andThen(~f=infoDecoder)