Experimental codec support for Thoth.Json 🧪
Why use codecs?
- Easier to keep encoding and decoding in sync
- Less code in many cases
- Clearer semantics when both encoding and decoding are required
Install from NuGet for Fable or .NET:
# Fable
dotnet add package Thoth.Json.Codec
# .NET
dotnet add package Thoth.Json.Net.Codec
Or using Paket:
# Fable
paket add Thoth.Json.Codec
# .NET
paket add Thoth.Json.Net.Codec
This library is built around a simple type definition:
type Codec<'t> =
{
Encoder : Encoder<'t>
Decoder : Decoder<'t>
}
Remember that a well-formed codec will allow an arbitary number of encoding-decoding round-trips.
First, open the namespace:
#if FABLE_COMPILER
open Thoth.Json.Codec
#else
open Thoth.Json.Net.Codec
#endif
Now you can create a codec from existing encoders and decoders like so:
let codec = Codec.create Encode.string Decode.string
However, it is recommended to use the built-in primitives.
Codec.int
Codec.bool
Codec.string
// etc...
You can encode values like this:
let json =
123
|> Encode.codec Codec.int
|> Encode.toString 2
And decode JSON like this:
let decoded =
"true"
|> Decode.fromString (Decode.codec Codec.bool)
Object codecs, typically used for Records, can be constructed using the objectCodec
Computation Expression:
type FooBar =
{
Foo : int
Bar : string
}
module FooBar =
let codec : Codec<FooBar> =
objectCodec {
let! foo = Codec.field "foo" (fun x -> x.Foo) Codec.int
and! bar = Codec.field "bar" (fun x -> x.Bar) Codec.string
return
{
Foo = foo
Bar = bar
}
}
The JSON looks like this:
{
"foo": 123,
"bar": "abc"
}
Note the use of and!
Variants, such as Discriminated Unions, should be constructed using the variantCodec
Computation Expression:
type Shape =
| Square of width : int
| Rectangle of width : int * height : int
module Shape =
let codec : Codec<Shape> =
variantCodec {
let! square = Codec.case "square" Square Codec.int
and! rectangle = Codec.case "rectangle" Rectangle (Codec.tuple2 Codec.int Codec.int)
return
function
| Square w -> square w
| Rectangle (w, h) -> rectangle (w, h)
}
Again, note the use of and!
With the above codec, the case value will be encoded to a property with the name of the tag.
In other words, the JSON will look like:
{
"square": 16
}
{
"rectangle": [
3,
4
]
}
If you prefer an object with tag
and value
properties, you can do the following:
module Shape =
let codec : Codec<Shape> =
variantCodecWithEncoding (TagAndValue ("tag", "value")) {
let! square = Codec.case "square" Square Codec.int
and! rectangle = Codec.case "rectangle" Rectangle (Codec.tuple2 Codec.int Codec.int)
return
function
| Square w -> square w
| Rectangle (w, h) -> rectangle (w, h)
}
This gives JSON like so:
{
"tag": "square",
"value": 16
}
{
"tag": "rectangle",
"value": [
3,
4
]
}
Codecs can be generated automatically.
type FooBar =
{
Foo : int
Bar : bool
Baz : string list
}
module FooBar =
let codec : Codec<FooBar> = Codec.Auto.generateCodec(CamelCase)
Beware that at this time, the generated codec may not guarantee the round-trip property!