Header menu logo JDeck

Codecs and Serialization

Note: Please refer to the [Encoding] and [Decoding] guides for more information on the encoding and decoding process.

The main point of JDeck is to avoid the need for manual serialization and deserialization where possible but, it isn't limited to that, you can go the full codec route if that's what you feel like. In the end this is just a thin wrapper around the System.Text.Json serialization library.

While We could provide an interface somewhat like:

type ICodec<'T> =
  abstract member Encoder: Encoder<'T>
  abstract member Decoder: Decoder<'T>

I think that would be pushing a bit too much wha the library is about. Instead, we provide a few helper functions to make it easier to work and these are located in the Codec module.

Internally JDeck has the following converter:

type private JDeckConverter<'T>(?encoder: Encoder<'T>, ?decoder: Decoder<'T>) =
  inherit JsonConverter<'T>()

  override _.CanConvert(typeToConvert: Type) = typeToConvert = typeof<'T>

  override _.Read(reader: byref<Utf8JsonReader>, _: Type, _) =
    match decoder with
    | Some decoder ->
      use json = JsonDocument.ParseValue(&reader)

      match decoder json.RootElement with
      | Ok value -> value
      | Error err -> raise(DecodingException(err))
    | None -> JsonSerializer.Deserialize<'T>(&reader)

  override _.Write
    (writer: Utf8JsonWriter, value, options: JsonSerializerOptions)
    =
    match encoder with
    | Some encoder -> encoder value |> _.WriteTo(writer)
    | None -> JsonSerializer.Serialize(writer, value, options)

Meaning that you can register coders and decoders Just for the type you're having trouble with, you don't really need to create a codec for each F# type you have in your project. using them is quite simple:

type ContactForm =
  | NoContact
  | Email of string
  | Phone of string

Our Discriminated union has different shapes, two of them match the other doesn't so we have to come up with a format that can handle all of them

  static member Encoder: Encoder<ContactForm> =
    fun (contactForm: ContactForm) ->
      Json.object [
        // Let's settle on the { "type": <string>, "value": <string> | null } format
        match contactForm with
        | NoContact ->
          "type", Encode.string "no-contact"
          "value", Encode.Null()
        | Email email ->
          "type", Encode.string "email"
          "value", Encode.string email
        | Phone phone ->
          "type", Encode.string "phone"
          "value", Encode.string phone
      ]

Our decoder will also have to accomodate to that, since we control both the serialization and deserialization we can be sure that the format will be consistent

  static member Decoder: Decoder<ContactForm> =
    fun json -> decode {
      let! type' = json |> Required.Property.get("type", Required.string)
      let! value = json |> Required.Property.get("value", Optional.string)

      match type', value with
      | "email", Some value -> return Email value
      | "phone", Some value -> return Phone value
      | "no-contact", None -> return NoContact
      | _ ->
        return!
          DecodeError.ofError(json.Clone(), "Invalid contact form type")
          |> Error
    }

Once we have our type and codec in place

type Person = {
  name: string
  age: int
  contactForms: ContactForm list
}

let person = {
  name = "John Doe"
  age = 30
  // Keep in mind that business rules may dictate
  // that a person can only have multiple contact forms but,
  // it shouldn't have NoContact alongside Email or Phone.
  // We'll add it here for demonstration purposes.
  contactForms = [ Email "abc@def.com"; Phone "123456789090"; NoContact ]
}

While using Decoding.fromString is tempting to use here we'll demonstrate that we can use the default JsonSerializer.(De)Serialize methods

let json =
  JsonSerializerOptions(PropertyNameCaseInsensitive = true)
  |> Codec.useCodec(ContactForm.Encoder, ContactForm.Decoder)

let str = JsonSerializer.Serialize(person, json)
let person' = JsonSerializer.Deserialize<Person>(str, json)

printfn $"%s{str}"

The output will be (albeit minified, but we'll format it for demonstration purposes):

{
  "name": "John Doe",
  "age": 30,
  "contactForms": [
    { "type": "email", "value": "abc@def.com" },
    { "type": "phone", "value": "123456789090" },
    { "type": "no-contact", "value": null }
  ]
}

In the case of the person' assignation, it should look like this:

printfn $"%A{person'}"

{
  name = "John Doe"
  age = 30
  contactForms = [ Email "abc@def.com"; Phone "123456789090"; NoContact ]
}

There's also Codec.useEncoder and Codec.useDecoder for when you only need to encode or decode a type, respectively.

Full Codec Way

If for whatever reason you'd prefer to go more manual than automatic, you can use the helper functions in the Decoding type.

// Equivalent to JsonSerializer.Deserialize("""{"id": "1234abcd"}""")
Decoding.auto("""{"id": "1234abcd"}""")

Decoding.fromString("""{"id": "1234abcd"}""", fun json -> decode {
  let! id = json |> Required.Property.get("id", Required.string)
  return { id }
})

Decoding.fromBytes(byteArray,
  fun json -> decode {
    let! id = json |> Required.Property.get("id", Required.string)
    return { id }
  }
)

And also Decoding.fromStream. Except from Decoding.auto all of these methods require a decoder to be passed in, which most likely means that you already know the shape of the json object you're trying to decode and also supplied the required codecs for each property and its type.

namespace System
namespace System.Text
namespace System.Text.Json
namespace System.Text.Json.Nodes
namespace System.Text.Json.Serialization
namespace JDeck
module Encode from JDeck.Encoding
<summary>Provides functions for encoding values to JSON nodes.</summary>
val Null: unit -> JsonNode
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>
type ICodec<'T> = abstract Decoder: Decoder<'T> abstract Encoder: Encoder<'T>
'T
type Encoder<'T> = 'T -> JsonNode
type Decoder<'TResult> = JsonElement -> Result<'TResult,DecodeError>
Multiple items
type private JDeckConverter<'T> = inherit JsonConverter<'T> new: ?encoder: Encoder<'T> * ?decoder: Decoder<'T> -> JDeckConverter<'T> override CanConvert: typeToConvert: Type -> bool override Read: reader: byref<Utf8JsonReader> * Type * JsonSerializerOptions -> 'T override Write: writer: Utf8JsonWriter * value: 'T * options: JsonSerializerOptions -> unit

--------------------
private new: ?encoder: Encoder<'T> * ?decoder: Decoder<'T> -> JDeckConverter<'T>
val encoder: Encoder<'T> option
val decoder: Decoder<'T> option
Multiple items
type JsonConverter = override CanConvert: typeToConvert: Type -> bool member Type: Type
<summary>Converts an object or value to or from JSON.</summary>

--------------------
type JsonConverter<'T> = inherit JsonConverter member CanConvert: typeToConvert: Type -> bool override Read: reader: byref<Utf8JsonReader> * typeToConvert: Type * options: JsonSerializerOptions -> 'T member ReadAsPropertyName: reader: byref<Utf8JsonReader> * typeToConvert: Type * options: JsonSerializerOptions -> 'T override Write: writer: Utf8JsonWriter * value: 'T * options: JsonSerializerOptions -> unit member WriteAsPropertyName: writer: Utf8JsonWriter * value: 'T * options: JsonSerializerOptions -> unit member HandleNull: bool member Type: Type
<summary>Converts an object or value to or from JSON.</summary>
<typeparam name="T">The type of object or value handled by the converter.</typeparam>


--------------------
type JsonConverterAttribute = inherit JsonAttribute new: converterType: Type -> unit member CreateConverter: typeToConvert: Type -> JsonConverter member ConverterType: Type
<summary>When placed on a property or type, specifies the converter type to use.</summary>

--------------------
JsonConverter() : JsonConverter<'T>

--------------------
JsonConverterAttribute(converterType: Type) : JsonConverterAttribute
val typeToConvert: Type
type Type = inherit MemberInfo interface IReflect member Equals: o: obj -> bool + 1 overload member FindInterfaces: filter: TypeFilter * filterCriteria: obj -> Type array member FindMembers: memberType: MemberTypes * bindingAttr: BindingFlags * filter: MemberFilter * filterCriteria: obj -> MemberInfo array member GetArrayRank: unit -> int member GetConstructor: bindingAttr: BindingFlags * binder: Binder * callConvention: CallingConventions * types: Type array * modifiers: ParameterModifier array -> ConstructorInfo + 3 overloads member GetConstructors: unit -> ConstructorInfo array + 1 overload member GetDefaultMembers: unit -> MemberInfo array override GetElementType: unit -> Type ...
<summary>Represents type declarations: class types, interface types, array types, value types, enumeration types, type parameters, generic type definitions, and open or closed constructed generic types.</summary>
val typeof<'T> : Type
val reader: byref<Utf8JsonReader>
type byref<'T> = (# "<Common IL Type Omitted>" #)
union case Option.Some: Value: 'T -> Option<'T>
val decoder: Decoder<'T>
val json: JsonDocument
type JsonDocument = interface IDisposable member Dispose: unit -> unit member WriteTo: writer: Utf8JsonWriter -> unit static member Parse: utf8Json: ReadOnlySequence<byte> * ?options: JsonDocumentOptions -> JsonDocument + 4 overloads static member ParseAsync: utf8Json: Stream * ?options: JsonDocumentOptions * ?cancellationToken: CancellationToken -> Task<JsonDocument> static member ParseValue: reader: byref<Utf8JsonReader> -> JsonDocument static member TryParseValue: reader: byref<Utf8JsonReader> * document: byref<JsonDocument> -> bool member RootElement: JsonElement
<summary>Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.</summary>
JsonDocument.ParseValue(reader: byref<Utf8JsonReader>) : JsonDocument
property JsonDocument.RootElement: JsonElement with get
<summary>Gets the root element of this JSON document.</summary>
<returns>A <see cref="T:System.Text.Json.JsonElement" /> representing the value of the document.</returns>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val value: 'T
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val err: DecodeError
val raise: exn: Exception -> 'T
exception DecodingException of DecodeError
union case Option.None: Option<'T>
type JsonSerializer = static member Deserialize: utf8Json: Stream * jsonTypeInfo: JsonTypeInfo -> obj + 39 overloads static member DeserializeAsync: utf8Json: Stream * jsonTypeInfo: JsonTypeInfo * ?cancellationToken: CancellationToken -> ValueTask<obj> + 4 overloads static member DeserializeAsyncEnumerable<'TValue> : utf8Json: Stream * topLevelValues: bool * ?options: JsonSerializerOptions * ?cancellationToken: CancellationToken -> IAsyncEnumerable<'TValue> + 3 overloads static member Serialize: utf8Json: Stream * value: obj * jsonTypeInfo: JsonTypeInfo -> unit + 14 overloads static member SerializeAsync: utf8Json: Stream * value: obj * jsonTypeInfo: JsonTypeInfo * ?cancellationToken: CancellationToken -> Task + 9 overloads static member SerializeToDocument: value: obj * jsonTypeInfo: JsonTypeInfo -> JsonDocument + 4 overloads static member SerializeToElement: value: obj * jsonTypeInfo: JsonTypeInfo -> JsonElement + 4 overloads static member SerializeToNode: value: obj * jsonTypeInfo: JsonTypeInfo -> JsonNode + 4 overloads static member SerializeToUtf8Bytes: value: obj * jsonTypeInfo: JsonTypeInfo -> byte array + 4 overloads static member IsReflectionEnabledByDefault: bool
<summary>Provides functionality to serialize objects or value types to JSON and to deserialize JSON into objects or value types.</summary>
JsonSerializer.Deserialize<'TValue>(reader: byref<Utf8JsonReader>, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(reader: byref<Utf8JsonReader>, ?options: JsonSerializerOptions) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(node: JsonNode, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(node: JsonNode, ?options: JsonSerializerOptions) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(element: JsonElement, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(element: JsonElement, ?options: JsonSerializerOptions) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(document: JsonDocument, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(document: JsonDocument, ?options: JsonSerializerOptions) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(json: string, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(json: string, ?options: JsonSerializerOptions) : 'TValue
   (+0 other overloads)
val writer: Utf8JsonWriter
Multiple items
type Utf8JsonWriter = interface IAsyncDisposable interface IDisposable new: bufferWriter: IBufferWriter<byte> * ?options: JsonWriterOptions -> unit + 1 overload member Dispose: unit -> unit member DisposeAsync: unit -> ValueTask member Flush: unit -> unit member FlushAsync: ?cancellationToken: CancellationToken -> Task member Reset: unit -> unit + 2 overloads member WriteBase64String: utf8PropertyName: ReadOnlySpan<byte> * bytes: ReadOnlySpan<byte> -> unit + 3 overloads member WriteBase64StringValue: bytes: ReadOnlySpan<byte> -> unit ...
<summary>Provides a high-performance API for forward-only, non-cached writing of UTF-8 encoded JSON text.</summary>

--------------------
Utf8JsonWriter(bufferWriter: Buffers.IBufferWriter<byte>, ?options: JsonWriterOptions) : Utf8JsonWriter
Utf8JsonWriter(utf8Json: IO.Stream, ?options: JsonWriterOptions) : Utf8JsonWriter
val options: JsonSerializerOptions
Multiple items
type JsonSerializerOptions = new: unit -> unit + 2 overloads member AddContext<'TContext (requires default constructor and 'TContext :> JsonSerializerContext)> : unit -> unit member GetConverter: typeToConvert: Type -> JsonConverter member GetTypeInfo: ``type`` : Type -> JsonTypeInfo member MakeReadOnly: unit -> unit + 1 overload member TryGetTypeInfo: ``type`` : Type * typeInfo: byref<JsonTypeInfo> -> bool member AllowOutOfOrderMetadataProperties: bool member AllowTrailingCommas: bool member Converters: IList<JsonConverter> member DefaultBufferSize: int ...
<summary>Provides options to be used with <see cref="T:System.Text.Json.JsonSerializer" />.</summary>

--------------------
JsonSerializerOptions() : JsonSerializerOptions
JsonSerializerOptions(defaults: JsonSerializerDefaults) : JsonSerializerOptions
JsonSerializerOptions(options: JsonSerializerOptions) : JsonSerializerOptions
val encoder: Encoder<'T>
JsonSerializer.Serialize<'TValue>(value: 'TValue, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : string
   (+0 other overloads)
JsonSerializer.Serialize<'TValue>(value: 'TValue, ?options: JsonSerializerOptions) : string
   (+0 other overloads)
JsonSerializer.Serialize(value: obj, jsonTypeInfo: Metadata.JsonTypeInfo) : string
   (+0 other overloads)
JsonSerializer.Serialize<'TValue>(writer: Utf8JsonWriter, value: 'TValue, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : unit
   (+0 other overloads)
JsonSerializer.Serialize<'TValue>(writer: Utf8JsonWriter, value: 'TValue, ?options: JsonSerializerOptions) : unit
   (+0 other overloads)
JsonSerializer.Serialize<'TValue>(utf8Json: IO.Stream, value: 'TValue, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : unit
   (+0 other overloads)
JsonSerializer.Serialize<'TValue>(utf8Json: IO.Stream, value: 'TValue, ?options: JsonSerializerOptions) : unit
   (+0 other overloads)
JsonSerializer.Serialize(writer: Utf8JsonWriter, value: obj, jsonTypeInfo: Metadata.JsonTypeInfo) : unit
   (+0 other overloads)
JsonSerializer.Serialize(value: obj, inputType: Type, context: JsonSerializerContext) : string
   (+0 other overloads)
JsonSerializer.Serialize(value: obj, inputType: Type, ?options: JsonSerializerOptions) : string
   (+0 other overloads)
Multiple items
val string: value: 'T -> string

--------------------
type string = String
type ContactForm = | NoContact | Email of string | Phone of string static member Decoder: Decoder<ContactForm> static member Encoder: Encoder<ContactForm>
val contactForm: ContactForm
Multiple items
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
<summary>Provides functions for creating JSON nodes.</summary>

--------------------
type JsonAttribute = inherit Attribute
<summary>Provides the base class for serialization attributes.</summary>
static member Json.object: values: Collections.Generic.KeyValuePair<string,JsonNode> seq -> JsonObject
static member Json.object: values: (string * JsonNode) seq -> JsonObject
union case ContactForm.NoContact: ContactForm
Multiple items
module Encode from Codecs

--------------------
module Encode from JDeck.Encoding
<summary>Provides functions for encoding values to JSON nodes.</summary>
val string: value: string -> JsonNode
Multiple items
val Null: unit -> JsonNode

--------------------
val Null: unit -> JsonNode
union case ContactForm.Email: string -> ContactForm
val email: string
union case ContactForm.Phone: string -> ContactForm
val phone: string
val json: JsonElement
val decode: DecodeBuilder
<summary> Computation expression to seamlessly decode JSON elements. </summary>
<example><code lang="fsharp"> type Person = { Name: string; Age: int } let PersonDecoder = decode { let! name = Property.get "name" Decode.Required.string and! age = Property.get "age" Decode.Required.int return { Name = name; Age = age } } </code></example>
val type': string
module Required from JDeck.Decode
<summary> Contains a set of decoders that are required to decode to the particular type otherwise the decoding will fail. </summary>
type Property = static member array: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult array,DecodeError>) + 1 overload static member get: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult,DecodeError>) + 1 overload static member list: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult list,DecodeError>) + 1 overload static member seq: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult seq,DecodeError>) + 1 overload
<summary> This type containes methods that are particularly useful to decode properties from JSON elements. They can be primitive properties, objects, arrays, etc. </summary>
<remarks> If the property is not found in the JSON element, the decoding will fail. </remarks>
static member Required.Property.get: name: string * decoder: CollectErrorsDecoder<'TResult> -> (JsonElement -> Result<'TResult,DecodeError list>)
static member Required.Property.get: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult,DecodeError>)
val string: Decoder<string>
val value: string option
module Optional from JDeck.Decode
<summary> Contains a set of decoders that are not required to decode to the particular type and will not fail. These decoders will return an option type. even if the value is null or is absent from the JSON element. </summary>
val string: Decoder<string option>
val value: string
Multiple items
module DecodeError from JDeck

--------------------
type DecodeError = { value: JsonElement kind: JsonValueKind rawValue: string targetType: Type message: string exn: exn option index: int option property: string option } member Equals: DecodeError * IEqualityComparer -> bool
<summary> In case of failure when a type is being decoded, this type is meant to contain the relevant information about the error. </summary>
val ofError<'TResult> : el: JsonElement * message: string -> DecodeError
JsonElement.Clone() : JsonElement
type Person = { name: string age: int contactForms: ContactForm list }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
type 'T list = List<'T>
val person: Person
val json: JsonSerializerOptions
module Codec from JDeck
val useCodec: encoder: Encoder<'T> * decoder: Decoder<'T> -> options: JsonSerializerOptions -> JsonSerializerOptions
property ContactForm.Encoder: Encoder<ContactForm> with get
property ContactForm.Decoder: Decoder<ContactForm> with get
val str: string
val person': Person
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val id: x: 'T -> 'T

Type something to start searching.