Header menu logo JDeck

Using with FsToolkit.ErrorHandling

One of the most popular libraries for workflows in F# is [FsToolkit.ErrorHandling], this library provides several computation expressions to handle errors in a functional way.

JDeck uses the Result type to handle errors, so it's easy to integrate with FsToolkit.ErrorHandling, Our decode {} computation expression is basically just a result CE in disguise and constrained to a particular error type.

It works for our purposes when you don't want the dependency on the library however, our CE is severely limitated to our particular use case. If you're already using FsToolkit in your codebase we recommend using the result {} or the validation {} CE instead.

let jDeckDecoder =
  fun el -> decode { // built-in CE
    let! value = Required.int el
    return {| value = value |}
  }

let fsToolkitDecoder =
  fun el -> result { // FsToolkit CE
    let! value = Required.int el
    return {| value = value |}
  }

let fsToolkitValDecoder =
  fun el -> validation { // FsToolkit CE
    let! value = Required.int el
    return {| value = value |}
  }

As you can see, the above decoders are completely equivalent with the exception of the validation one, this returns a list of errors instead of a single error. Given that the result CE is a drop-in replacement for the decode CE, from now on we'll focus on the validation CE.

Validations

For cases where you'd like to keep decoding after an error for further recollection, this is the way to go. You may have seen some functions and methods with col or collect in their names, these are meant to be used with the validation {} CE.

// For example let's say we want to decode the payload of a posted user to our server
type User = { username: string; emails: string seq }

First we define some rules for our validations, in the case of the username we want to be sure that it is not an empty string, and it is within a certain limit of characters. These validations here are arbitrary but you should be able to see how can you enforce your own domain rules when decoding the json objects

let usernameRules (value: string) (el: JsonElement) = validation {
  let! _ =
    value
    |> Result.requireNotEmpty "Name cannot be empty"
    |> Result.mapError(fun msg -> DecodeError.ofError(el.Clone(), msg))

  and! _ =
    value.Length >= 6
    |> Result.requireTrue "username has to be at last 6 characters"
    |> Result.mapError(fun msg -> DecodeError.ofError(el.Clone(), msg))

  and! _ =
    value.Length <= 20
    |> Result.requireTrue "username has to be at most 20 characters"
    |> Result.mapError(fun msg -> DecodeError.ofError(el.Clone(), msg))

  return ()
}

In the case of the emails, we will do very simple validations but as you may imagine, you can validate domains, against a regex, and even if it already exists if you pass the correct information to this validation.

let emailRules (index: int, value: string) (el: JsonElement) = result {
  let! _ =
    value.Contains("@")
    |> Result.requireTrue $"Element at {index} - must contain @"
    |> Result.mapError(fun msg -> DecodeError.ofIndexed(el.Clone(), index, msg))

  and! _ =
    value.Contains(".")
    |> Result.requireTrue $"Element at {index} - must contain ."
    |> Result.mapError(fun msg -> DecodeError.ofIndexed(el.Clone(), index, msg))

  return ()
}

Then we can decode the payload and apply the validations Keep in mind that we're using strings for simplicity here but the errors should match the actual error of your domain types

let bindJson (reqBody: string) = validation {
  use document = JsonDocument.Parse(reqBody)
  let json = document.RootElement

  let! username =
    let decoder =
      fun el -> validation {
        let! value = Required.string el
        do! usernameRules value el
        return value
      }

    Required.Property.get ("username", decoder) json

  let! emails =
    // for validation to work we need to wrap the decoders in a validation {} CE
    // this is because we can't define overloads based on the return type.
    let decoder =
      fun (index: int) el -> validation {
        // decode the element as a string
        let! email = Required.string el

        // validate that it is a valid email according to our rules
        do! emailRules (index, email) el

        // return a validated email
        return email
      }

    json |> Required.Property.get("emails", Decode.sequenceCol(decoder))

  return { username = username; emails = emails }
}

When we apply this decoder which is also validating our rules to the following JSON string, we expect it to fail, but rather than telling us a single error it will collect the ones available and report them together.

// { "username": "John Doe", "emails": ["email1@email.com", null, "email2email.com", "not-an-email", null] }
match bindJson reqBody with
| Ok user -> printfn "User: %A" user
| Error errors -> printfn "Errors: %A" (errors |> List.map _.message)
// Errors: [
//   "Expected 'String' but got `Null`"
//   "Element at 2 - must contain @"
//   "Element at 3 - must contain @"
//   "Expected 'String' but got `Null`"
// ]

If we provide a non-null string list then we're able to see just the errors that correspond to our validations

// { "username": "John Doe", "emails": ["email1@email.com", "email2@email.com", "not-an-email", "email4@emailcom] }
match bindJson reqBody2 with
| Ok user -> printfn "User: %A" user
| Error errors -> printfn "Errors: %A" (errors |> List.map _.message)
// Errors: ["Element at 2 - must contain @"; "Element at 3 - must contain ."]

Note: Sometimes for simplicity, folks use strings as the resulting error, it is recommended that you provide a more meaningful and information rich type for your errors, as these will be passed on potentially over several layers and the information could be lost if you don't provide a proper type.

namespace System
namespace System.IO
namespace System.Text
namespace System.Text.Json
namespace FsToolkit
namespace FsToolkit.ErrorHandling
namespace JDeck
val jDeckDecoder: el: JsonElement -> Result<{| value: int |},DecodeError>
val el: 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 value: int
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>
val int: Decoder<int>
val fsToolkitDecoder: el: JsonElement -> Result<{| value: int |},DecodeError>
val result: ResultBuilder
<summary> The <c>Result</c> computation expression. </summary>
val fsToolkitValDecoder: el: JsonElement -> Validation<{| value: int |},DecodeError>
val validation: ValidationBuilder
type User = { username: string emails: string seq }
Multiple items
val string: value: 'T -> string

--------------------
type string = String
Multiple items
val seq: sequence: 'T seq -> 'T seq

--------------------
type 'T seq = Collections.Generic.IEnumerable<'T>
val usernameRules: value: string -> el: JsonElement -> Validation<unit,DecodeError>
val value: string
[<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 FsToolkit.ErrorHandling
<summary> Helper functions for working with <c>Result</c> values. </summary>

--------------------
module Result from Microsoft.FSharp.Core

--------------------
[<Struct>] type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val requireNotEmpty: error: 'error -> xs: #('value seq) -> Result<unit,'error>
<summary> Returns <c>Ok</c> if the sequence is not empty, or the specified error if it is. Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/result/requirefunctions#requirenotempty</href></summary>
<param name="error">The error value to return if the sequence is empty.</param>
<param name="xs">The sequence to check.</param>
<returns>An <c>Ok</c> result if the sequence is not empty, otherwise an <c>Error</c> result with the specified error value.</returns>
Multiple items
val mapError: [<InlineIfLambda>] errorMapper: ('errorInput -> 'errorOutput) -> input: Result<'ok,'errorInput> -> Result<'ok,'errorOutput>
<summary> Maps the error value of a <c>Result</c>to a new error value using the specified error mapper function. Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/result/maperror</href></summary>
<param name="errorMapper">The function that maps the input error value to the output error value.</param>
<param name="input">The <c>Result</c>value to map the error value of.</param>
<returns>A new <c>Result</c>with the same Ok value and the mapped error value.</returns>


--------------------
val mapError: mapping: ('TError -> 'U) -> result: Result<'T,'TError> -> Result<'T,'U>
val msg: 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
property String.Length: int with get
val requireTrue: error: 'error -> value: bool -> Result<unit,'error>
<summary> Requires a boolean value to be true, otherwise returns an error result. Documentation is found here: <href>https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoolkit.errorhandling/result/requirefunctions#requiretrue</href></summary>
<param name="error">The error value to return if the condition is false.</param>
<param name="value">The boolean value to check.</param>
<returns>An <c>Ok</c> result if the condition is true, otherwise an Error result with the specified error value.</returns>
val emailRules: index: int * value: string -> el: JsonElement -> Result<unit,DecodeError>
val index: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
String.Contains(value: string) : bool
String.Contains(value: char) : bool
String.Contains(value: string, comparisonType: StringComparison) : bool
String.Contains(value: char, comparisonType: StringComparison) : bool
val ofIndexed<'TResult> : el: JsonElement * index: int * message: string -> DecodeError
val bindJson: reqBody: string -> Validation<User,DecodeError>
val reqBody: string
val document: 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.Parse(json: string, ?options: JsonDocumentOptions) : JsonDocument
JsonDocument.Parse(json: ReadOnlyMemory<char>, ?options: JsonDocumentOptions) : JsonDocument
JsonDocument.Parse(utf8Json: ReadOnlyMemory<byte>, ?options: JsonDocumentOptions) : JsonDocument
JsonDocument.Parse(utf8Json: Stream, ?options: JsonDocumentOptions) : JsonDocument
JsonDocument.Parse(utf8Json: Buffers.ReadOnlySequence<byte>, ?options: JsonDocumentOptions) : JsonDocument
val json: JsonElement
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>
val username: string
val decoder: el: JsonElement -> Validation<string,DecodeError>
val string: Decoder<string>
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 emails: string seq
val decoder: index: int -> el: JsonElement -> Validation<string,DecodeError>
val email: string
Multiple items
module Decode from JDeck.Decode

--------------------
module Decode from JDeck
val sequenceCol: [<InlineIfLambda>] decoder: IndexedCollectErrorsDecoder<'a> -> el: JsonElement -> Result<'a seq,DecodeError list>
<summary> Decodes a JSON array element into the value of type <typeparamref name="TResult" />. </summary>
<remarks> If a failure is encountered in the decoding process, the error is collected and the decoding continues however, the result will be an error containing a list of all the errors that occurred during the decoding process. </remarks>
<param name="decoder"></param>
<param name="el"></param>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val user: User
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val errors: DecodeError list
Multiple items
module List from FsToolkit.ErrorHandling

--------------------
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val map: mapping: ('T -> 'U) -> list: 'T list -> 'U list
val reqBody2: string

Type something to start searching.