Header menu logo JDeck

Decoding Guide

When you use STJ to deserialize JSON, you would likely expect to Just call

type MyType = { id: string }

let myObject = JsonSerializer.Deserialize<MyType>("""{"id":"a1b2c3d4"}""")

And call it a day, and that's fine for most of the cases specially if you own the server which is producing the JSON.

There are a few cases where you might want to customize the deserialization process, for example:

In these cases, you can customize the deserialization process by manually mapping the JSON to your F# types.

Automatic Decoding

Before we dive into manual decoding, let's see how you can automatically decode a JSON string into an F# type. using normal means of deserialization. In addition to the JsonSerializer.Deserialize method, you can use the Decoding type provided by JDeck to decode JSON strings into F# types.

let myobj = Decoding.auto<MyType>("""{"id":"a1b2c3d4"}""")

The auto method calls JsonDocument.Parse(jsonString) internally and then deserializes the JSON object into the provided type. It works the same way as JsonSerializer.Deserialize.

For cases where you want to customize the deserialization process, you can register decoders in a JsonSerializerOptions instance and pass it to the auto method. For the next case, let's assume that for some reason we need a special decoding process for the MyType type.

let options = JsonSerializerOptions() |> Codec.useDecoder<MyType>(myDecoder)

let myobj2 = Decoding.auto<MyType>("""{"id":"a1b2c3d4"}""", options)
// Or
let myobj3 =
  JsonSerializer.Deserialize<MyType>("""{"id":"a1b2c3d4"}""", options)

In this way, you're able to customize the deserialization process for a specific type.

Speaking of decoders, a decoder is defined as:

type Decoder<'TResult> = JsonElement -> Result<'TResult, DecodeError>

Where JsonElement is the type representing a JSON object in STJ. where 'TResult is the type you want to decode the JSON into. For example a Decoder<int> would map a JSON string to an integer.

Required Vs Optional

A Decoder can be defined as required or optional. A required decoder will fail if the data does not match the expected type, and if it is missing from the JSON string, while an optional decoder will not fail if the data is missing or has a null value.

As an example let's see the following two values.

let value = Decoding.fromString("10", Required.int)
printfn $"Value: %A{value}" // Value: Ok 10

let noValue = Decoding.fromString("null", Optional.int)
printfn $"Value: %A{noValue}" // Value: Ok None

As you see, the value is successful and the noValue is successful as well, but it is None. given that null is not a valid integer in F#. However, if you try to decode a value that is not an integer with an optional integer decoder, it will fail.

let invalidValue = Decoding.fromString("\"abc\"", Optional.int)
printfn $"Value: %A{invalidValue}"
// Error { value = abc
//         kind = String
//         rawValue = "\"abc\""
//         targetType = System.Object
//         message = "Expected 'Number' but got `String`"
//         exn = None
//         index = None
//         property = None }

With this you can be sure that the data you are decoding is of the expected type, even if it is missing or not.

Decoding JSON Objects

As you're already aware, decoding primitives returns results, and this means that in order to decode a JSON object you need to do it only on successful results. Thus needing to nest and generate pyramids of match expressions which is not ideal not funny, unmaintainable and cumbersome.

Note: In general we recommend that you use FsToolkit.ErrorHandling's result {} computation expression to handle the errors and the results of the decoders. In a very seamless way. Please refer to the FsToolkit Section for more information.

For cases like those and if you want to avoid the dependency on FsToolkit.ErrorHandling, you can use the built-in decode {} computation expression. we provide.

The decode {} computation expression is a way to chain multiple decoders together in a single expression, and it will short-circuit if any of the decoders fail.

type Person = {
  name: string
  age: int
  email: string option
}

let objectDecoder: Decoder<Person> =
  fun jsonElement -> decode {
    let! name = Required.Property.get ("name", Required.string) jsonElement
    and! age = Required.Property.get ("age", Required.int) jsonElement
    // An optional property means that the key "emails" can be missing from the JSON object
    // However, if the key is present it must comply with the decoder (in this case a string)
    and! email = Optional.Property.get ("emails", Required.string) jsonElement

    return {
      name = name
      age = age
      email = email
    }
  }

Another way to decode the above object is to expect the email key to be present in the document, but it can be null.

let objDecoder2 jsonElement = decode {
  let! name = Required.Property.get ("name", Required.string) jsonElement
  and! age = Required.Property.get ("age", Required.int) jsonElement
  // Now the key must be present in the JSON object, but it can be null.
  and! email = Required.Property.get ("emails", Optional.string) jsonElement

  return {
    name = name
    age = age
    email = email
  }
}

Decoding Discriminated Unions

Decoding a discriminated union is a bit complex as it may be represented in many shapes or forms, there isn't really a general concensus on how to represent a discriminated union in JSON and that's the reason it is not supported by default in STJ. Let's define the following discriminated union.

type PageStatus =
  | Idle
  | Loading
  | FailedWith of string
  | Special of int

How do you represent this in JSON? a string or an array with a string and a value? or an object with a key and a value? however you decide to represent it, you need to write a decoder for it.

For cases like this, we provide a helper function called oneOf which takes a list of decoders and tries to decode the JSON object with each decoder until one of them succeeds.

First let's define the decoders for the PageStatus type.

module PageStatus =
  // decodes {"status": "idle"}
  let idleAndLoadingDecoder el = decode {
    let! value = Required.string el

    match value with
    | "idle" -> return Idle
    | "loading" -> return Loading
    | _ ->
      return!
        DecodeError.ofError(
          el.Clone(),
          "The provided value is not either idle or loading"
        )
        |> Result.Error
  }

  // decodes {"status": ["failed-with", "message"] }
  // decodes {"status": ["special", <int status>] }
  let failedOrSpecialDecoder el = decode {
    return!
      el
      |> Required.Property.get(
        "status",
        fun el -> decode {
          let! type' = Decode.decodeAt Required.string 0 el

          match type' with
          | "failed-with" ->
            return!
              Decode.decodeAt Required.string 1 el |> Result.map FailedWith
          | "special" ->
            return! Decode.decodeAt Required.int 1 el |> Result.map Special
          | _ ->
              return!
                DecodeError.ofError(
                  el,
                  "The provided value is not either \"failed-with\" or \"special\""
                )
                |> Error
        }
      )
  }

Note: Don't forget to call .Clone() on the JsonElement when you're returning an error, as the JsonElement may come from a JsonDocument which is a disposable type. If you don't clone it, you run the risk of the JsonElement being disposed before you can use it.

As you can see, the idleAndLoadingDecoder decoder expects a JSON object with a key status and a value of either idle or loading, while the failedDecoder expects a JSON object with a key status and a value of an array with two elements, the first element is failed-with and the second element is the message.

Now let's use the oneOf function to decode the JSON object.

let decodedValue =
  Decoding.fromString(
    """{"status": "idle"}""",
    Decode.oneOf [
      PageStatus.failedOrSpecialDecoder
      PageStatus.idleAndLoadingDecoder
    ]
  )
// val decodedValue : Result<PageStatus, DecodeError> = Ok Idle

While the order of the decoders in the oneOf is not important, since we're in a stop-on-first-success mode, it is recommended to put the most likely decoders to succeed first.

namespace System
namespace System.Text
namespace System.Text.Json
namespace System.Text.Json.Serialization
namespace JDeck
type MyType = { id: string }
val id: x: 'T -> 'T
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val myObject: MyType
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: Nodes.JsonNode, jsonTypeInfo: Metadata.JsonTypeInfo<'TValue>) : 'TValue
   (+0 other overloads)
JsonSerializer.Deserialize<'TValue>(node: Nodes.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 myobj: Result<MyType,DecodeError>
type Decoding = static member auto: json: string * ?docOptions: JsonDocumentOptions -> Result<'TResult,DecodeError> + 5 overloads static member fromBytes: value: byte array * options: JsonDocumentOptions * decoder: (JsonElement -> 'TResult) -> 'TResult + 1 overload static member fromBytesCol: value: byte array * options: JsonDocumentOptions * decoder: CollectErrorsDecoder<'TResult> -> Result<'TResult,DecodeError list> + 1 overload static member fromStream: value: Stream * options: JsonDocumentOptions * decoder: (JsonElement -> 'TResult) -> Task<'TResult> + 1 overload static member fromStreamCol: value: Stream * options: JsonDocumentOptions * decoder: CollectErrorsDecoder<'TResult> -> Task<Result<'TResult,DecodeError list>> + 1 overload static member fromString: value: string * options: JsonDocumentOptions * decoder: Decoder<'TResult> -> Result<'TResult,DecodeError> + 1 overload static member fromStringCol: value: string * options: JsonDocumentOptions * decoder: CollectErrorsDecoder<'TResult> -> Result<'TResult,DecodeError list> + 1 overload
static member Decoding.auto: json: System.IO.Stream * ?docOptions: JsonDocumentOptions -> System.Threading.Tasks.Task<Result<'TResult,DecodeError>>
static member Decoding.auto: json: byte array * ?docOptions: JsonDocumentOptions -> Result<'TResult,DecodeError>
static member Decoding.auto: json: string * ?docOptions: JsonDocumentOptions -> Result<'TResult,DecodeError>
static member Decoding.auto: json: System.IO.Stream * options: JsonSerializerOptions * ?docOptions: JsonDocumentOptions -> System.Threading.Tasks.Task<Result<'TResult,DecodeError>>
static member Decoding.auto: json: byte array * options: JsonSerializerOptions * ?docOptions: JsonDocumentOptions -> Result<'TResult,DecodeError>
static member Decoding.auto: json: string * options: JsonSerializerOptions * ?docOptions: JsonDocumentOptions -> Result<'TResult,DecodeError>
val myDecoder: jsonElement: JsonElement -> Result<MyType,DecodeError>
val jsonElement: 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 id: 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 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
module Codec from JDeck
val useDecoder: decoder: Decoder<'T> -> options: JsonSerializerOptions -> JsonSerializerOptions
val myobj2: Result<MyType,DecodeError>
val myobj3: MyType
type Decoder<'TResult> = JsonElement -> Result<'TResult,DecodeError>
[<Struct>] type JsonElement = member Clone: unit -> JsonElement member EnumerateArray: unit -> ArrayEnumerator member EnumerateObject: unit -> ObjectEnumerator member GetArrayLength: unit -> int member GetBoolean: unit -> bool member GetByte: unit -> byte member GetBytesFromBase64: unit -> byte array member GetDateTime: unit -> DateTime member GetDateTimeOffset: unit -> DateTimeOffset member GetDecimal: unit -> decimal ...
<summary>Represents a specific JSON value within a <see cref="T:System.Text.Json.JsonDocument" />.</summary>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
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 value: Result<int,DecodeError>
static member Decoding.fromString: value: string * decoder: Decoder<'TResult> -> Result<'TResult,DecodeError>
static member Decoding.fromString: value: string * options: JsonDocumentOptions * decoder: Decoder<'TResult> -> Result<'TResult,DecodeError>
val int: Decoder<int>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val noValue: Result<int option,DecodeError>
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 int: Decoder<int option>
val invalidValue: Result<int option,DecodeError>
type Person = { name: string age: int email: string option }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
type 'T option = Option<'T>
val objectDecoder: jsonElement: JsonElement -> Result<Person,DecodeError>
val name: string
val age: int
val email: string option
type Property = static member array: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult array option,DecodeError>) + 1 overload static member get: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult option,DecodeError>) + 1 overload static member list: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult list option,DecodeError>) + 1 overload static member seq: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult seq option,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 or is null in the JSON element, the decoding will return an option type. </remarks>
static member Optional.Property.get: name: string * decoder: CollectErrorsDecoder<'TResult> -> (JsonElement -> Result<'TResult option,DecodeError list>)
static member Optional.Property.get: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult option,DecodeError>)
val objDecoder2: jsonElement: JsonElement -> Result<Person,DecodeError>
val string: Decoder<string option>
type PageStatus = | Idle | Loading | FailedWith of string | Special of int
val idleAndLoadingDecoder: el: JsonElement -> Result<PageStatus,DecodeError>
val el: JsonElement
val value: string
union case PageStatus.Idle: PageStatus
union case PageStatus.Loading: PageStatus
val ofError<'TResult> : el: JsonElement * message: string -> DecodeError
JsonElement.Clone() : JsonElement
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val failedOrSpecialDecoder: el: JsonElement -> Result<PageStatus,DecodeError>
val type': string
Multiple items
module Decode from JDeck.Decode

--------------------
module Decode from JDeck
val decodeAt: [<InlineIfLambda>] decoder: Decoder<'TResult> -> index: int -> el: JsonElement -> Result<'TResult,DecodeError>
<summary> Attempts to decode a JSON element that is living inside an array at the given index. </summary>
<param name="decoder"></param>
<param name="index"></param>
<param name="el"></param>
val map: mapping: ('T -> 'U) -> result: Result<'T,'TError> -> Result<'U,'TError>
union case PageStatus.FailedWith: string -> PageStatus
union case PageStatus.Special: int -> PageStatus
val decodedValue: Result<PageStatus,DecodeError>
val oneOf: decoders: Decoder<'TResult> seq -> Decoder<'TResult>
<summary> Takes a list of possible decoders and tries to decode the JSON element with each one of them. </summary>
<remarks> This is useful to decode JSON elements into discriminated unions </remarks>
<param name="decoders"></param>
Multiple items
module PageStatus from Decoding

--------------------
type PageStatus = | Idle | Loading | FailedWith of string | Special of int

Type something to start searching.