Header menu logo JDeck

Encoding Guide

While encoding is not really a big concern for STJ there's still types that are not supported out of the box. As usual... Discriminated unions are at the center of the stage here.

That being said, JDeck provides a way to encode values to JSON strings in a similar fashion to the decoders.

An Encoder is defined as:

type Encoder<'T> = 'T -> JsonNode

There's two styles offered currently by this library:

Available Encoders

JDeck provides encoders for a wide range of primitive types:

let propStyleEncoder =
  Json.object [
    "name", Encode.string "John Doe"
    "age", Encode.int 42
    ("profile", Json.object [ "id", Encode.guid(Guid.NewGuid()) ])
  ]

The property list style is basically just a recollection of key-value pairs in a list.

let pipeStyleEncoder =
  Json.empty()
  |> Encode.property("name", Encode.string "John Doe")
  |> Encode.property("age", Encode.int 42)
  |> Encode.property(
    "profile",
    Json.empty() |> Encode.property("id", Encode.guid(Guid.NewGuid()))
  )

The pipeline style is basically a "builder" like pattern where you start with an empty object and keep adding properties to it. Both styles are equivalent, and you can choose the one that fits your style better you can even mix and match both! though I wouldn't recommend that.

Let's see a more meaningful example, we'll encode a Person object. First let's define a couple of types to work with:

type Address = {
  street: string
  city: string
  zip: string option
} with

  static member Encoder: Encoder<Address> =
    fun address ->
      Json.object [
        "street", Encode.string address.street
        "city", Encode.string address.city
        match address.zip with
        | Some zip -> "zip", Encode.string zip
        | None -> ()
      ]

It is recommended to define an encoder for whatever type you want to encode in order to keep your code less verbose in the main encoder.

type ContactMethod =
  | Email of string
  | Phone of string

  static member Encoder: Encoder<ContactMethod> =
    fun contactMethod ->
      Json.object [
        match contactMethod with
        | Email email ->
          "type", Encode.string "email"
          "value", Encode.string email
        | Phone phone ->
          "type", Encode.string "phone"
          "value", Encode.string phone
      ]

As we've discussed in other sections of this website, discriminated unions are a particular type that needs special handling when working with System.Text.Json APIs as it is not supported.

Now let's define the Person type and its encoder:

type Person = {
  name: string
  age: int
  address: Address
  contactMethod: ContactMethod list
} with

  static member Encoder: Encoder<Person> =
    fun person ->
      Json.object [
        "name", Encode.string person.name
        "age", Encode.int person.age
        // here we use our previously defined encoders
        "address", Address.Encoder person.address
        // for each contact method we encode it using the ContactMethod encoder
        "contactMethod",
        Json.sequence(person.contactMethod, ContactMethod.Encoder)
      ]

The defined encoder for the Person type uses the previously defined encoders for the Address and ContactMethod types. For other discriminated unions and custom types you can customize entirely the shape of the final JSON object.

let encodedPerson = Person.Encoder person
printfn $"%s{encodedPerson.ToJsonString()}"

The final JSON object will look like this:

{
  "name": "John Doe",
  "age": 42,
  "address": { "street": "21 2nd Street", "city": "New York", "zip": "10021" },
  "contactMethod": [
    { "type": "email", "value": "abc@dfg.com" },
    { "type": "phone", "value": "1234567890" }
  ]
}

The JSON object above has been formatted for display purposes, but the actual JSON string will be minified if no options are supplied to the ToJsonString method.

Mixed-Type Arrays

Sometimes you need to encode arrays with mixed types, such as discriminated unions represented as [tag, value] tuples. JDeck provides the Json.sequence overload that accepts a sequence of already-encoded JsonNode values, allowing you to mix different types in a single array.

// Encoding a mixed-type array for a discriminated union
type Shape =
  | Box of width: float32 * height: float32
  | Circle of radius: float32

  static member Encoder: Encoder<Shape> =
    fun shape ->
      match shape with
      | Box(w, h) ->
        Json.sequence [ Encode.string "Box"; Encode.single w; Encode.single h ]
      | Circle r -> Json.sequence [ Encode.string "Circle"; Encode.single r ]

let boxShape = Shape.Encoder(Box(10.5f, 20.3f))
printfn $"%s{boxShape.ToJsonString()}" // ["Box",10.5,20.3]

let circleShape = Shape.Encoder(Circle(5.0f))
printfn $"%s{circleShape.ToJsonString()}" // ["Circle",5.0]

The Json.sequence overload for JsonNode seq is particularly useful when encoding discriminated unions where array elements have different types (e.g., a string tag followed by numeric or object values).

Alternatively, you can use Encode.mixedSeq to add encoded values to an existing JsonArray:

let arr = JsonArray()
Encode.mixedSeq [ Encode.int 1; Encode.string "hello"; Encode.boolean true ] arr
printfn $"%s{arr.ToJsonString()}" // [1,"hello",true]

This is particularly useful when you need to incrementally build up a mixed-type array or when working with pre-existing JsonArray instances.

The encoding story is not set in stone yet for JDeck, and there's still room for improvement, feedback is appreciated in this regard.

namespace System
namespace JDeck
namespace System.Text
namespace System.Text.Json
namespace System.Text.Json.Nodes
type Encoder<'T> = 'T -> JsonNode
'T
type JsonNode = member AsArray: unit -> JsonArray member AsObject: unit -> JsonObject member AsValue: unit -> JsonValue member DeepClone: unit -> JsonNode member GetElementIndex: unit -> int member GetPath: unit -> string member GetPropertyName: unit -> string member GetValue<'T> : unit -> 'T member GetValueKind: unit -> JsonValueKind member ReplaceWith<'T> : value: 'T -> unit ...
<summary>The base class that represents a single node within a mutable JSON document.</summary>
val propStyleEncoder: JsonObject
type Json = static member empty: unit -> JsonObject static member object: values: (string * JsonNode) seq -> JsonObject + 1 overload static member sequence: values: 'T seq * encoder: Encoder<'T> -> JsonNode + 1 overload
<summary>Provides functions for creating JSON nodes.</summary>
static member Json.object: values: Collections.Generic.KeyValuePair<string,JsonNode> seq -> JsonObject
static member Json.object: values: (string * JsonNode) seq -> JsonObject
module Encode from JDeck.Encoding
<summary>Provides functions for encoding values to JSON nodes.</summary>
val string: value: string -> JsonNode
val int: value: int -> JsonNode
val guid: value: Guid -> JsonNode
Multiple items
[<Struct>] type Guid = new: b: byte array -> unit + 6 overloads member CompareTo: value: Guid -> int + 1 overload member Equals: g: Guid -> bool + 1 overload member GetHashCode: unit -> int member ToByteArray: unit -> byte array + 1 overload member ToString: unit -> string + 2 overloads member TryFormat: utf8Destination: Span<byte> * bytesWritten: byref<int> * ?format: ReadOnlySpan<char> -> bool + 1 overload member TryWriteBytes: destination: Span<byte> -> bool + 1 overload static member (<) : left: Guid * right: Guid -> bool static member (<=) : left: Guid * right: Guid -> bool ...
<summary>Represents a globally unique identifier (GUID).</summary>

--------------------
Guid ()
Guid(b: byte array) : Guid
Guid(b: ReadOnlySpan<byte>) : Guid
Guid(g: string) : Guid
Guid(b: ReadOnlySpan<byte>, bigEndian: bool) : Guid
Guid(a: int, b: int16, c: int16, d: byte array) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid.NewGuid() : Guid
val pipeStyleEncoder: JsonObject
static member Json.empty: unit -> JsonObject
val property: name: string * value: JsonNode -> jsonObject: JsonObject -> JsonObject
Multiple items
val string: value: 'T -> string

--------------------
type string = String
type 'T option = Option<'T>
type Address = { street: string city: string zip: string option } static member Encoder: Encoder<Address>
val address: Address
Address.street: string
Address.city: string
Address.zip: string option
union case Option.Some: Value: 'T -> Option<'T>
val zip: string
union case Option.None: Option<'T>
type ContactMethod = | Email of string | Phone of string static member Encoder: Encoder<ContactMethod>
val contactMethod: ContactMethod
union case ContactMethod.Email: string -> ContactMethod
val email: string
union case ContactMethod.Phone: string -> ContactMethod
val phone: string
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
type 'T list = List<'T>
type Person = { name: string age: int address: Address contactMethod: ContactMethod list } static member Encoder: Encoder<Person>
val person: Person
Person.name: string
Person.age: int
property Address.Encoder: Encoder<Address> with get
Person.address: Address
static member Json.sequence: values: JsonNode seq -> JsonNode
static member Json.sequence: values: 'T seq * encoder: Encoder<'T> -> JsonNode
Person.contactMethod: ContactMethod list
property ContactMethod.Encoder: Encoder<ContactMethod> with get
val encodedPerson: JsonNode
property Person.Encoder: Encoder<Person> with get
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
JsonNode.ToJsonString(?options: Text.Json.JsonSerializerOptions) : string
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

--------------------
type float32 = Single

--------------------
type float32<'Measure> = float32
type Shape = | Box of width: float32 * height: float32 | Circle of radius: float32 static member Encoder: Encoder<Shape>
val shape: Shape
union case Shape.Box: width: float32 * height: float32 -> Shape
val w: float32
val h: float32
val single: value: single -> JsonNode
union case Shape.Circle: radius: float32 -> Shape
val r: float32
val boxShape: JsonNode
property Shape.Encoder: Encoder<Shape> with get
val circleShape: JsonNode
val arr: JsonArray
Multiple items
type JsonArray = inherit JsonNode interface ICollection<JsonNode> interface IEnumerable<JsonNode> interface IEnumerable interface IList<JsonNode> new: ?options: Nullable<JsonNodeOptions> -> unit + 4 overloads member Add: item: JsonNode -> unit + 1 overload member Clear: unit -> unit member Contains: item: JsonNode -> bool member GetEnumerator: unit -> IEnumerator<JsonNode> ...
<summary>Represents a mutable JSON array.</summary>

--------------------
JsonArray(?options: Nullable<JsonNodeOptions>) : JsonArray
JsonArray(items: ReadOnlySpan<JsonNode>) : JsonArray
JsonArray([<ParamArray>] items: JsonNode array) : JsonArray
JsonArray(options: JsonNodeOptions, items: ReadOnlySpan<JsonNode>) : JsonArray
JsonArray(options: JsonNodeOptions, [<ParamArray>] items: JsonNode array) : JsonArray
val mixedSeq: values: JsonNode seq -> jsonArray: JsonArray -> JsonNode
val boolean: value: bool -> JsonNode

Type something to start searching.