When using auto coders, we are referring to both auto encoders and auto decoders.
Thoth.Json
This documentation is for Thoth.Json
v4, documentation for older versions can be found here:
Decoder
Turn JSON values into F# values.
By using a Decoder, you will be guaranteed that the JSON structure is correct. This is especially useful if you use Fable without sharing your domain with the server.
This module is inspired by Json.Decode from Elm and elm-decode-pipeline.
You can also take a look at the Elm documentation.
What is a Decoder?
Here is the signature of a Decoder
:
type Decoder<'T> = string -> obj -> Result<'T, DecoderError>
This is taking two arguments:
- the traveled path
- an "untyped" value and checking if it has the expected structure.
If the structure is correct, then you get an Ok
result, otherwise an Error
explaining where and why the decoder failed.
Example of a decoder error:
Error at: `$.user.firstname`
Expecting an object with path `user.firstname` but instead got:
{
"user": {
"name": "maxime",
"age": 25
}
}
Node `firstname` is unknown.
The path generated is a valid JSONPath
, so you can use tools like JSONPath Online Evaluator to explore your JSON.
Primitives decoders
string : Decoder<string>
guid : Decoder<System.Guid>
int : Decoder<int>
int64 : Decoder<int64>
uint64 : Decoder<uint64>
bigint : Decoder<bigint>
bool : Decoder<bool>
float : Decoder<float>
decimal : Decoder<decimal>
datetime : Decoder<System.DateTime>
datetimeOffset : Decoder<System.DateTimeOffset>
timespan : Decoder<System.TimeSpan>
open Thoth.Json
> Decode.fromString Decode.string "\"maxime\""
val it : Result<string, string> = Ok "maxime"
> Decode.fromString Decode.int "25"
val it : Result<int, string> = Ok 25
> Decode.fromString Decode.bool "true"
val it : Result<bool, string> = Ok true
> Decode.fromString Decode.float "true"
val it : Result<float, string> = Error "Error at: `$`\n Expecting a float but instead got: true"
With these primitives decoders we can handle basic JSON values.
Collections
There are special decoders for the following collections.
list : Decoder<'value> -> Decoder<'value list>
array : Decoder<'value> -> Decoder<'value array>
index : -> int -> Decoder<'value> -> Decoder<'value>
open Thoth.Json
> Decode.fromString (array int) "[1, 2, 3]"
val it : Result<int [], string> = Ok [|1, 2, 3|]
> Decode.fromString (list string) """["Maxime", "Alfonso", "Vesper"]"""
val it : Result<string list, string> = Ok ["Maxime", "Alfonso", "Vesper"]
> Decode.fromString (Decode.index 1 Decode.string) """["maxime", "alfonso", "steffen"]"""
val it : Result<string, string> = Ok("alfonso")
Decoding Objects
In order to decode objects, you can use:
field : string -> Decoder<'value> -> Decoder<'value>
- Decode a JSON object, requiring a particular field.
at : string list -> Decoder<'value> -> Decoder<'value>
- Decode a JSON object, requiring certain path.
open Thoth.Json
> Decode.fromString (field "x" int) """{"x": 10, "y": 21}"""
val it : Result<int, string> = Ok 10
> Decode.fromString (field "y" int) """{"x": 10, "y": 21}"""
val it : Result<int, string> = Ok 21
Important:
These two decoders only take into account the provided field or path. The object can have other fields/paths with other content.
Map functions
To get data from several fields and convert them into a record you will need to use the map
functions:
map2
, map3
, ..., map8
.
open Thoth.Json
type Point =
{ X : int
Y : int }
static member Decoder : Decoder<Point> =
Decode.map2 (fun x y ->
{ X = x
Y = y } : Point)
(Decode.field "x" Decode.int)
(Decode.field "y" Decode.int)
> Decode.fromString Point.Decoder """{"x": 10, "y": 21}"""
val it : Result<Point, string> = Ok { X = 10; Y = 21 }
Object builder style
When working with larger objects, you can use the object builder helper.
open Thoth.Json
type User =
{ Id : int
Name : string
Email : string
Followers : int }
static member Decoder : Decoder<User> =
Decode.object
(fun get ->
{ Id = get.Required.Field "id" Decode.int
Name = get.Optional.Field "name" Decode.string
|> Option.defaultValue ""
Email = get.Required.Field "email" Decode.string
Followers = 0 }
)
> Decode.fromString User.Decoder """{ "id": 67, "email": "user@mail.com" }"""
val it : Result<User, string> = Ok { Id = 67; Name = ""; Email = "user@mail.com"; Followers = 0 }
Encoder
Module for turning F# values into JSON values.
This module is inspired by Json.Encode from Elm.
How to use it?
open Thoth.Json
let person =
Encode.object
[ "firstname", Encode.string "maxime"
"surname", Encode.string "mangel"
"age", Encode.int 25
"address", Encode.object
[ "street", Encode.string "main street"
"city", Encode.string "Bordeaux" ]
]
let compact = Encode.toString 0 person
// {"firstname":"maxime","surname":"mangel","age":25,"address":{"street":"main street","city":"Bordeaux"}}
let readable = Encode.toString 4 person
// {
// "firstname": "maxime",
// "surname": "mangel",
// "age": 25,
// "address": {
// "street": "main street",
// "city": "Bordeaux"
// }
// }
Auto coders
If your JSON structure is a one to one match with your F# type, then you can use auto coders.
Auto decoder
Auto decoders will generate the decoder at runtime for you and still guarantee that the JSON structure is correct.
> let json = """{ "Id" : 0, "Name": "maxime", "Email": "mail@domain.com", "Followers": 0 }"""
> Decode.Auto.fromString<User>(json)
val it : Result<User, string> = Ok { Id = 0; Name = "maxime"; Email = "mail@domain.com"; Followers = 0 }
Decode.Auto
helpers accept an optional argument caseStrategy
that applies to keys:
CamelCase
, then the keys in the JSON are consideredcamelCase
PascalCase
, then the keys in the JSON are consideredPascalCase
SnakeCase
, then the keys in the JSON are consideredsnake_cases
> let json = """{ "id" : 0, "name": "maxime", "email": "mail@domain.com", "followers": 0 }"""
> Decode.Auto.fromString<User>(json, caseStrategy=CamelCase)
val it : Result<User, string> = Ok { Id = 0; Name = "maxime"; Email = "mail@domain.com"; Followers = 0 }
Auto encoder
Auto decoders will generate the encoder at runtime for you.
type User =
{ Id : int
Name : string
Email : string
Followers : int }
let user =
{ Id = 0
Name = "maxime"
Email = "mail@domain.com"
Followers = 0 }
let json = Encode.Auto.toString(4, user)
// {
// "Id": 0,
// "Name": "maxime",
// "Email": "mail@domain.com",
// "Followers": 0
// }
Extra coders
When generating an auto coder, sometimes you will want to use a manual coder for a type nested in your domain hierarchy.
In those cases you can pass extra coders that will replace the default (or missing) coders. Use the Extra
module to build the map for extra coders (see example below).
let myExtraCoders =
Extra.empty
|> Extra.withCustom MyType.Encode MyType.Decode
Decoding int64, decimal or bigint
Coders for int64
, decimal
or bigint
won't be automatically generated by default. If they were, the bundle size would increase significantly even if you don't intend to use these types.
If required, you can easily include them in your extra coders with the helpers in the Extra
module:
let myExtraCoders =
Extra.empty
|> Extra.withInt64
|> Extra.withDecimal
|> Extra.withCustom MyType.Encode MyType.Decode
Caching
To avoid having to regenerate your auto coders every time you need them, you can use the helpers with the Cached
suffix instead (please note in these cases you shouldn't change the value of extra parameters like caseStrategy
or extra
).
The easiest way to do it is to include some helpers in your app to easily generate (or retrieve from cache) coders whenever you need them. For example:
// Note the helpers must be inlined to resolve generic parameters in Fable
let inline encoder<'T> = Encode.Auto.generateEncoderCached<'T>(caseStrategy = CamelCase, extra = myExtraCoders)
let inline decoder<'T> = Decode.Auto.generateDecoderCached<'T>(caseStrategy = CamelCase, extra = myExtraCoders)
Now you can easily invoke the helpers whenever you need a coder. Most of the times you can omit the generic argument as it will be inferred.
let demo(x : RequestType) : ResponseType =
let requestJson = encoder x |> Encode.toString 4
let responseJson = (* Send JSON to server and receive response *)
Decode.unsafeFromString decoder responseJson
.Net & NetCore support
You can share your decoders and encoders between your client and server.
In order to use Thoth.Json API on .Net or NetCore you need to use the Thoth.Json.Net
package.
Code sample
// By adding this condition, you can share your code between your client and server
#if FABLE_COMPILER
open Thoth.Json
#else
open Thoth.Json.Net
#endif
type User =
{ Id : int
Name : string
Email : string
Followers : int }
static member Decoder : Decode.Decoder<User> =
Decode.object
(fun get ->
{ Id = get.Required.Field "id" Decode.int
Name = get.Optional.Field "name" Decode.string
|> Option.defaultValue ""
Email = get.Required.Field "email" Decode.string
Followers = 0 }
)
static member Encoder (user : User) =
Encode.object
[ "id", Encode.int user.Id
"name", Encode.string user.Name
"email", Encode.string user.Email
"followers", Encode.int user.Followers
]
Giraffe
If you're using the Giraffe or Saturn web servers, you can use the Thoth.Json.Giraffe
package to enable automatic JSON serialization with Thoth in your responses. Check how to add a custom serializer to Giraffe.
The
ThothSerializer
type also includes some static helpers to deal with JSON directly for request and response streams.