Header menu logo JDeck

JDeck a System.Text.Json wrapper

JDeck is a Thoth.Json-like Json decoder based on System.Text.Json in a single file with no external dependencies. Plays well with other libraries that use System.Text.Json like FSharp.SystemTextJson

Note: While JDeck has no dependencies to start working right away, it is recommended to use FsToolkit.ErrorHandling

Usage

For most F# types, you can use the Decode.auto function to decode JSON as shown below:

#r "nuget: JDeck, 1.0.0-beta-*"

open JDeck

type Person = {
  name: string
  age: int
  emails: string list
}
let json = """{"name": "Alice", "age": 30, "emails": ["alice@name.com", "alice@age.com"] }"""

let result: Result<Person, DecodeError> = Decoding.auto(json)

match result with
| Ok person -> printfn $"Person: %A{person}"
| Error err -> printfn $"Error: %A{err}"

In cases where the data is inconclusive, you deserialize Discriminated Unions or does not play well with F# immutability, you can create a manual decoder.

#r "nuget: JDeck, 1.0.0-beta-*"

open System.Text.Json
open JDeck

type Person = {
  Name: string
  Age: int
  Emails: string list
}
type ServerResponse = { Data: Person; Message: string }

let personDecoder: Decoder<Person> = fun  person -> decode {
  let! name = person |> Required.Property.get("name", Optional.string)
  and! age = person |> Required.Property.get("age", Required.int)
  and! emails = person |> Required.Property.list("emails", Optional.string)
  return {
    Name = name |> Option.defaultValue "<missing name>"
    Age = age
    // Remove any optional value from the list
    Emails = emails |> List.choose id
  }
}
// Inconclusive data coming from the server
let person = """{"name": null, "age": 30, "emails": ["alice@name.com", "alice@age.com", null] }"""

let result: Result<ServerResponse, DecodeError> =
  // ServerResponse will decode automatically while Person will use the custom decoder
  Decoding.auto(
    $$"""{ "data": {{person}}, "message": "Success" }""",
    // Include your own decoder
    JsonSerializerOptions(PropertyNameCaseInsensitive = true) |> Codec.useDecoder personDecoder
  )

match result with
| Ok person -> printfn $"Person: %A{person}"
| Error err -> printfn $"Error: %A{err}"

Acknowledgements

Nothing is done in the void, in this case I'd like to thank the following libraries for their inspiration and ideas:

namespace JDeck
type Person = { name: string age: int emails: string list }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
type 'T list = List<'T>
val json: string
val result: Result<Person,DecodeError>
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>
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: System.Text.Json.JsonDocumentOptions -> System.Threading.Tasks.Task<Result<'TResult,DecodeError>>
static member Decoding.auto: json: byte array * ?docOptions: System.Text.Json.JsonDocumentOptions -> Result<'TResult,DecodeError>
static member Decoding.auto: json: string * ?docOptions: System.Text.Json.JsonDocumentOptions -> Result<'TResult,DecodeError>
static member Decoding.auto: json: System.IO.Stream * options: System.Text.Json.JsonSerializerOptions * ?docOptions: System.Text.Json.JsonDocumentOptions -> System.Threading.Tasks.Task<Result<'TResult,DecodeError>>
static member Decoding.auto: json: byte array * options: System.Text.Json.JsonSerializerOptions * ?docOptions: System.Text.Json.JsonDocumentOptions -> Result<'TResult,DecodeError>
static member Decoding.auto: json: string * options: System.Text.Json.JsonSerializerOptions * ?docOptions: System.Text.Json.JsonDocumentOptions -> Result<'TResult,DecodeError>
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val person: Person
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val err: DecodeError
namespace System
namespace System.Text
namespace System.Text.Json
type ServerResponse = { Data: Person Message: string }
namespace Microsoft.FSharp.Data
val personDecoder: person: JsonElement -> Result<Person,DecodeError>
type Decoder<'TResult> = JsonElement -> Result<'TResult,DecodeError>
val person: 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 name: string option
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>)
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 age: int
val int: Decoder<int>
val emails: string option list
static member Required.Property.list: name: string * decoder: CollectErrorsDecoder<'TResult> -> (JsonElement -> Result<'TResult list,DecodeError list>)
static member Required.Property.list: name: string * decoder: Decoder<'TResult> -> (JsonElement -> Result<'TResult list,DecodeError>)
module Option from Microsoft.FSharp.Core
val defaultValue: value: 'T -> option: 'T option -> 'T
Multiple items
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 choose: chooser: ('T -> 'U option) -> list: 'T list -> 'U list
val id: x: 'T -> 'T
val person: string
val result: Result<ServerResponse,DecodeError>
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>
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 person: ServerResponse

Type something to start searching.