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):
|
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.
<summary>Provides functions for encoding values to JSON nodes.</summary>
<summary>The base class that represents a single node within a mutable JSON document.</summary>
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>
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
<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>
<summary>Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.</summary>
<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>
<summary>Provides functionality to serialize objects or value types to JSON and to deserialize JSON into objects or value types.</summary>
(+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)
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
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
(+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)
val string: value: 'T -> string
--------------------
type string = String
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: (string * JsonNode) seq -> JsonObject
module Encode from Codecs
--------------------
module Encode from JDeck.Encoding
<summary>Provides functions for encoding values to JSON nodes.</summary>
val Null: unit -> JsonNode
--------------------
val Null: unit -> JsonNode
<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> Contains a set of decoders that are required to decode to the particular type otherwise the decoding will fail. </summary>
<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: Decoder<'TResult> -> (JsonElement -> Result<'TResult,DecodeError>)
<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>
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 int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int