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:
- Thoth.Json for the inspiration and a cross-runtime solution to JSON decoding, compatible with F#, JS, and Python.
- FsToolkit.ErrorHandling for the general mechanism for dealing with Result types and Computation expressions
namespace JDeck
type Person =
{
name: string
age: int
emails: string list
}
Multiple items
val string: value: 'T -> string
--------------------
type string = System.String
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
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
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>
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>
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>
<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>
<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>
<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>)
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>
<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>)
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 ...
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>
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
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