For simplicity, we used Decode.string
but in a real world scenario, you would probably want to validate the email format.
Composition
By default, Thoth.Json provides support for most of the primitives types like int
, bool
, System.Guid
, etc.
In order to build more complex types, it offers several ways to compose decoders.
Object builder style
When working with objects, it is recommanded to use the object builder helper.
It has the benefit of putting the fields decoder and association at the same level. Indeed, when using map functions it is easy to mess up the arguments order.
It also supports an record with any number of properties.
When using the object builder API, you first choose if the property is required or optional.
Then you describe, for each property, how to decode it.
type Point =
{
X : int
Y : int
}
Decode.object (fun get ->
{
X = get.Required.Raw (Decode.field "x" Decode.int)
Y = get.Required.Raw (Decode.field "y" Decode.int)
}
)
Object builder also provides more friendly syntax for the most common cases.
The decoder from above can be written as:
Decode.object (fun get ->
{
X = get.Required.Field "x" Decode.int
Y = get.Required.Field "y" Decode.int
}
)
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": {
"author": {
"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
.
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
}
)
Now we combine them to form the parent record:
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.decode
Abstract = get.Required.Field "port" Post.decode
}
)
Decode.fromString
(Decode.field "data" Data.decoder)
json
Map functions
The map2
, map3
, ..., map8
functions take a function to build a concrete type from the result of the provided decoders.
Thoth.Json only provides map
function up to 8 arguments, if you need more consider using Object builder or implementing your own mapX
function.
Decode.map2 (fun x y ->
{
X = x
Y = y
}
)
(Decode.field "x" Decode.int)
(Decode.field "y" Decode.int)
Chain decoders
You can use andThen
in order to use the result of one decoder as the input of another one.
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}"""
)
The decoder will succeed only if the JSON value is a string and the string is "student" or "teacher".
Map result to another type
When using DDD (aka Domain Driven Design) you often need to map your types.
type Email = Email of string
module Email =
let decoder : Decoder<Email> =
Decode.string
|> Decode.map Email
If the provided JSON is a string, the decoder will succeed and return an Email type.
Inconsistent JSON
Sometimes it can happen that the JSON you received is not consistent or have several ways to represent a type.
In these cases, you can 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:
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
]
let json =
"[ 1, null, 2, 3 ]"
Decode.fromString (Decode.list nullableIntDecoder) json
// Returns:
// Ok [ 1, 0, 2, 3 ]