Composition
Thoth.Json provides a decoder for most primitive types, like int, bool or System.Guid.
To build decoders for your own types, it offers several ways to compose them.
Object builder style
When working with objects, use the object builder helper.
It puts the field name and its decoder at the same place. With map functions it's easy to get the argument order wrong.
It also supports a record with any number of properties.
You first choose whether the property is required or optional, then describe how to decode it.
open Thoth.Json.Core
open Thoth.Json.JavaScript
type Point =
{
X: int
Y: int
}
let decoder =
Decode.object (fun get ->
{
X = get.Required.Raw(Decode.field "x" Decode.int)
Y = get.Required.Raw(Decode.field "y" Decode.int)
}
)
Decode.fromString decoder """{ "x": 1, "y": 2 }""" |> Docs.print
The object builder also provides a friendlier syntax for the most common cases. The decoder above can be written as:
open Thoth.Json.Core
open Thoth.Json.JavaScript
type Point =
{
X: int
Y: int
}
let decoder =
Decode.object (fun get ->
{
X = get.Required.Field "x" Decode.int
Y = get.Required.Field "y" Decode.int
}
)
Decode.fromString decoder """{ "x": 1, "y": 2 }""" |> Docs.print
get.Required and get.Optional each offer three members.
Field
Decodes the value under the given property name.
At
Decodes the value under a path of property names.
get.Required.At [ "user"; "name" ] Decode.string
Raw
Runs the decoder against the object itself. Use it to reach a value the two others can't describe.
Combine decoders
If your data is composed of several objects, you can construct the decoders top down. First you create the decoders of the different records, then you combine them together.
If we have the following JSON:
{
"data": {
"user": {
"name": "Triss Merigold",
"age": 42
},
"post": {
"title": "Handle JSON with Fable",
"abstract": "How to simply read data with Thoth.Json"
}
}
}
We create types and decoders for User and Post, then combine them to form the parent record.
open Thoth.Json.Core
open Thoth.Json.JavaScript
let json =
"""
{
"data": {
"user": {
"name": "Triss Merigold",
"age": 42
},
"post": {
"title": "Handle JSON with Fable",
"abstract": "How to simply read data with Thoth.Json"
}
}
}
"""
type User =
{
Name: string
Age: int
}
module User =
let decoder: Decoder<User> =
Decode.object (fun get ->
{
Name = get.Required.Field "name" Decode.string
Age = get.Required.Field "age" Decode.int
}
)
type Post =
{
Title: string
Abstract: string
}
module Post =
let decoder: Decoder<Post> =
Decode.object (fun get ->
{
Title = get.Required.Field "title" Decode.string
Abstract = get.Required.Field "abstract" Decode.string
}
)
type Data =
{
User: User
Post: Post
}
module Data =
// Get both structures and decode them with their own decoder accordingly
let decoder: Decoder<Data> =
Decode.object (fun get ->
{
User = get.Required.Field "user" User.decoder
Post = get.Required.Field "post" Post.decoder
}
)
json
|> Decode.fromString (Decode.field "data" Data.decoder)
|> Docs.print
Computation expression
The decoder computation expression composes decoders without naming a combinator.
let! runs the decoders one after the other. and! runs them independently, which is what you want when a decoder doesn't depend on the result of the previous one.
open Thoth.Json.Core
open Thoth.Json.JavaScript
type Point =
{
X: int
Y: int
}
let decoder =
decoder {
let! x = Decode.field "x" Decode.int
and! y = Decode.field "y" Decode.int
return
{
X = x
Y = y
}
}
Decode.fromString decoder """{ "x": 1, "y": 2 }""" |> Docs.print
Map functions
The map2, map3, ..., map8 functions take a function to build a concrete type from the results of the provided decoders.
Thoth.Json only provides map functions up to 8 arguments. If you need more, consider using the object builder or the computation expression.
open Thoth.Json.Core
open Thoth.Json.JavaScript
type Point =
{
X: int
Y: int
}
let decoder =
Decode.map2
(fun x y ->
{
X = x
Y = y
}
)
(Decode.field "x" Decode.int)
(Decode.field "y" Decode.int)
Decode.fromString decoder """{ "x": 1, "y": 2 }""" |> Docs.print
Chain decoders
Use andThen to use the result of one decoder as the input of another one.
open Thoth.Json.Core
open Thoth.Json.JavaScript
type PersonType =
| Student
| Teacher
module PersonType =
let decoder: Decoder<PersonType> =
Decode.string
|> Decode.andThen (fun textValue ->
match textValue with
| "student" -> Decode.succeed Student
| "teacher" -> Decode.succeed Teacher
| invalid ->
Decode.fail
$"Expecting \"student\" or \"teacher\" but instead got: \"%s{invalid}\""
)
Decode.fromString PersonType.decoder "\"student\"" |> Docs.print
Decode.fromString PersonType.decoder "\"pilot\"" |> Docs.print
The decoder succeeds only if the JSON value is a string, and that string is "student" or "teacher".
Map result to another type
When using DDD (aka Domain Driven Design) you often need to map your types.
open Thoth.Json.Core
open Thoth.Json.JavaScript
type Email = Email of string
module Email =
let decoder: Decoder<Email> = Decode.string |> Decode.map Email
Decode.fromString Email.decoder "\"maxime@mail.com\"" |> Docs.print
If the provided JSON is a string, the decoder succeeds and returns an Email.
Inconsistent JSON
Sometimes the JSON you received is not consistent, or has several ways to represent a type.
In these cases, use Decode.oneOf to try different decoders.
Imagine you are parsing a list of numbers but some of them are represented as null.
[ 1, null, 2, 3 ]
You can write a decoder like that:
open Thoth.Json.Core
open Thoth.Json.JavaScript
let nullableIntDecoder: Decoder<int> =
Decode.oneOf
[
// First try to decode it as a standard int
Decode.int
// If it fails, try to decode it as a null
Decode.nil 0
]
"[ 1, null, 2, 3 ]"
|> Decode.fromString (Decode.list nullableIntDecoder)
|> Docs.print
Recursive types
A decoder for a type that refers to itself is built with Decode.fix. It hands you the decoder being defined, so you can use it inside its own definition.
open Thoth.Json.Core
open Thoth.Json.JavaScript
type Comment =
{
Text: string
Replies: Comment list
}
let decoder: Decoder<Comment> =
Decode.fix (fun self ->
Decode.object (fun get ->
{
Text = get.Required.Field "text" Decode.string
Replies = get.Required.Field "replies" (Decode.list self)
}
)
)
"""
{
"text": "Hello",
"replies": [
{ "text": "Hi", "replies": [] }
]
}
"""
|> Decode.fromString decoder
|> Docs.print