Skip to content

njlr/thoth-json-codec

Repository files navigation

Thoth.Json.Codec

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

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

Instructions

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)

Objects

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

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
  ]
}

Auto

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!

Releases

No releases published

Packages

No packages published

Languages