Welcome! I am Loïc Denuzière aka "Tarmil", and this is my technical blog.
I'm a French developer who's quite passionate about functional programming, and I've been using F# for most of my work for over ten years.
I've been one of the main developers of WebSharper, the F# web framework with both server-side and JavaScript-compiled client-side tools.
Recently I've been focusing more on Bolero, a library that runs F# applications in WebAssembly using Blazor, and other personal projects.
Follow me on Twitter, Mastodon and find me on GitHub.
I am happy to announce FSharp.SystemTextJson version 1.2!
FSharp.SystemTextJson is a library that provides support for F# types in .NET's standard System.Text.Json.
Here is a summary of the new features in v1.2.
Skippable Option fields (#154)
Version 1.1 introduced the method JsonFSharpOptions.WithSkippableOptionFields(?bool)
that, when set to true, causes None
and ValueNone
to always be skipped when serialized as the value of a record or union field.
However, even with this option left as false, None
and ValueNone
were still skipped if the JsonSerializerOptions
have DefaultIgnoreCondition
set to WhenWritingNull
.
In version 1.2, a new overload of JsonFSharpOptions.WithSkippableOptionFields
takes an enum as argument that brings more possibilities.
SkippableOptionFields.Always
is equivalent to true
: record and union fields equal to None
and ValueNone
are always skipped.
SkippableOptionFields.FromJsonSerializerOptions
is equivalent to false
: record and union fields equal to None
and ValueNone
are only skipped if JsonSerializerOptions
have DefaultIgnoreCondition
set to WhenWritingNull
.
Otherwise, they are serialized as JSON null
.
SkippableOptionFields.Never
is new: None
and ValueNone
are never skipped, and always serialized as JSON null
.
Handling of dictionary and map keys (#161 and #162)
In version 1.2, FSharp.SystemTextJson now makes use of System.Text.Json's ReadAsPropertyName
and WriteAsPropertyName
features.
This manifests in two ways:
Single-case unions can now be used as keys in a standard Dictionary
(and related types).
NOTE: This requires System.Text.Json 8.0.
let options = JsonFSharpOptions().ToJsonSerializerOptions()
type CountryCode = CountryCode of string
let countries = dict [
CountryCode "us", "United States"
CountryCode "fr", "France"
CountryCode "gb", "United Kingdom"
]
JsonSerializer.Serialize(countries, options)
// --> {"us":"United States","fr":"France","gb":"United Kingdom"}
The format for maps can now be customized using JsonFSharpOptions.WithMapFormat(MapFormat)
.
MapFormat.Object
always serializes maps as objects.
The key type must be supported as key for dictionaries.
NOTE: This requires System.Text.Json 8.0.
let options = JsonFSharpOptions().WithMapFormat(MapFormat.Object).ToJsonSerializerOptions()
let countries = Map [
Guid.NewGuid(), "United States"
Guid.NewGuid(), "France"
Guid.NewGuid(), "United Kingdom"
]
JsonSerializer.Serialize(countries, options)
// --> {"44e2a549-66c6-4515-970a-a1e85ce42624":"United States", ...
MapFormat.ArrayOfPairs
always serializes maps as JSON arrays whose items are [key,value]
pairs.
let options = JsonFSharpOptions().WithMapFormat(MapFormat.ArrayOfPairs).ToJsonSerializerOptions()
let countries = Map [
"us", "United States"
"fr", "France"
"uk", "United Kingdom"
]
JsonSerializer.Serialize(countries, options)
// --> [["us","United States"],["fr","France"],["uk","United Kingdom"]]
MapFormat.ObjectOrArrayOfPairs
is the default, and the same behavior as v1.1.
Maps whose keys are string or single-case unions wrapping string are serialized as JSON objects, and other maps are serialized as JSON arrays whose items are [key,value]
pairs.
Other improvements
#158: Throw an exception when trying to deserialize null
into a record or union in any context, rather than only when they are in fields of records and unions.
#163: Add StructuralComparison
to the type Skippable<_>
. This allows using it with types that are themselves marked with StructuralComparison
.
Bug fixes
#160: Fix WithSkippableOptionFields(false)
not working for voption
.
#164: When deserializing a record with JsonIgnoreCondition.WhenWritingNull
, when a non-nullable field is missing, throw a proper JsonException
that includes the name of the field, rather than a NullReferenceException
.
Happy coding!
By Loïc "Tarmil" Denuzière on Monday, August 14, 2023
fsharp release library fsharp-systemtextjson Tweet Permalink
I am happy to announce FSharp.SystemTextJson version 1.1!
FSharp.SystemTextJson is a library that provides support for F# types in .NET's standard System.Text.Json.
Here is a summary of the new features in v1.1.
Fluent configuration
The library now comes with a new syntax for configuration.
Instead of a constructor with a mix of optional arguments and enum flags, you can now use a more consistent fluent syntax.
- The baseline options are declared using one of these static methods on the
JsonFSharpOptions
type: Default()
, NewtonsoftLike()
, ThothLike()
or FSharpLuLike()
.
- Then, fluent instance methods set various options and return a new instance of
JsonFSharpOptions
.
- Finally, a converter with the given options can be either added to an existing
JsonSerializerOptions
with the method AddToJsonSerializerOptions()
, or to a new one with ToJsonSerializerOptions()
.
For example:
let options =
JsonFSharpOptions.Default()
.WithUnionInternalTag()
.WithUnionNamedFields()
.WithUnionTagName("type")
.ToJsonSerializerOptions()
type SomeUnion =
| SomeUnionCase of x: int * y: string
JsonSerializer.Serialize(SomeUnionCase (1, "test"), options)
// --> {"type":"SomeUnionCase","x":1,"y":"test"}
Note that in the future, newly added options will be available via the fluent configuration, but they may not always be added to the constructor syntax; especially because this can break binary compatibility (see this issue).
Skippable option fields
In version 1.0, the default behavior for fields of type option
and voption
changed: they are no longer serialized as a present or missing field, and instead as a null field.
While the pre-1.0 behavior can be recovered by using the JsonSerializerOptions
property DefaultIgnoreCondition
, this has other side-effects and multiple users have asked for a cleaner way to use options for missing fields.
This is now possible with the option SkippableOptionFields
. This is the first option that is only available via fluent configuration, and not as a JsonFSharpConverter
constructor argument.
let options =
JsonFSharpOptions.Default()
.WithSkippableOptionFields()
.ToJsonSerializerOptions()
JsonSerializer.Serialize({| x = Some 42; y = None |}, options)
// --> {"x":42}
Happy coding!
By Loïc "Tarmil" Denuzière on Saturday, January 21, 2023
fsharp release library fsharp-systemtextjson Tweet Permalink
More than three years after the first release, I am happy to announce FSharp.SystemTextJson version 1.0!
FSharp.SystemTextJson is a library that provides support for F# types in .NET's standard System.Text.Json.
Here is a summary of the new features in v1.0.
JsonName
attribute
System.Text.Json provides an attribute JsonPropertyName
to change the name of a property in JSON.
In FSharp.SystemTextJson 1.0, the new attribute JsonName
is equivalent but provides more functionality:
When used on a discriminated union case, JsonName
can take a value of type int
or bool
instead of string
.
type MyUnion =
| [<JsonName 1>] One of x: int
| [<JsonName 2>] Two of y: string
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.Default ||| JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields))
JsonSerializer.Serialize(Two "two", options)
// => {"Case":2,"x":"two"}
JsonName
can take multiple values.
When deserializing, all these values are treated as equivalent.
When serializing, the first one is used.
type Name =
{ [<JsonName("firstName", "first")>]
First: string
[<JsonName("lastName", "last")>]
Last: string }
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter())
JsonSerializer.Deserialize<Name>("""{"first":"John","last":"Doe"}""", options)
// => { First = "John"; Last = "Doe" }
JsonSerializer.Serialize({ First = "John"; Last = "Doe" }, options)
// => {"firstName":"John","lastName":"Doe"}
JsonName
has a settable property Field: string
.
It is used to set the JSON name of a union case field with the given name.
type Contact =
| [<JsonName("email", Field = "address")>]
Email of address: string
| Phone of number: string
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.Default ||| JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields))
JsonSerializer.Serialize(Email "john.doe@example.com")
// => {"Case":"Email","email":"john.doe@example.com"}
Record properties
By default, FSharp.SystemTextJson only serializes the fields of a record.
There are now two ways to also serialize their properties:
The option includeRecordProperties: bool
enables serializing all record properties (except those that have the attribute JsonIgnore
, just like fields).
type User =
{ id: int
name: string }
member this.profileUrl = $"https://example.com/user/{this.id}/{this.name}"
[<JsonIgnore>]
member this.notIncluded = "This property is not included"
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter(includeRecordProperties = true))
JsonSerializer.Serialize({ id = 1234; name = "john.doe" })
// => {"id":1234,"name":"john.doe","profileUrl":"https://example.com/user/1234/john.doe"}
The attribute JsonInclude
can be used on a specific property to serialize it.
type User =
{ id: int
name: string }
[<JsonInclude>]
member this.profileUrl = $"https://example.com/user/{this.id}/{this.name}"
member this.notIncluded = "This property is not included"
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter())
JsonSerializer.Serialize({ id = 1234; name = "john.doe" })
// => {"id":1234,"name":"john.doe","profileUrl":"https://example.com/user/1234/john.doe"}
BREAKING CHANGE: Missing fields
In FSharp.SystemTextJson 0.x, using default options, missing fields of type option
or voption
would be deserialized into None
or ValueNone
.
This was unintended behavior, which is corrected in version 1.0: these missing fields now throw an error.
To restore the previous behavior, either enable the option IgnoreNullValues = true
, or or use the type Skippable
instead of option
or voption
.
Additionally, the option DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
is now treated as a synonym for IgnoreNullValues = true
.
type Name =
{ firstName: string
lastName: string option }
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter())
JsonSerializer.Deserialize<Name>("""{"firstName":"John"}""", options)
// => JsonException
let options2 = JsonSerializerOptions(IgnoreNullValues = true)
options2.Converters.Add(JsonFSharpConverter())
JsonSerializer.Deserialize<Name>("""{"firstName":"John"}""", options2)
// => { firstName = "John"; lastName = None }
let options3 = JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)
options3.Converters.Add(JsonFSharpConverter())
JsonSerializer.Deserialize<Name>("""{"firstName":"John"}""", options3)
// => { firstName = "John"; lastName = None }
type NameWithSkippable =
{ firstName: string
lastName: Skippable<string> }
let options4 = JsonSerializerOptions()
options4.Converters.Add(JsonFSharpConverter())
JsonSerializer.Deserialize<Name>("""{"firstName":"John"}""", options4)
// => { firstName = "John"; lastName = Skip }
Built-in support in .NET 6 and JsonFSharpTypes
In .NET 6, support has been added in System.Text.Json for a number of F# types.
This support is different from FSharp.SystemTextJson in a number of ways:
- Records, tuples, lists, sets, maps:
null
is accepted by the deserializer, and returns a null value.
- Records: missing fields are deserialized to default value instead of throwing an error.
- Maps: only primitive keys are supported. Numbers and booleans are converted to string and used as JSON objet keys.
- Tuples: only supports up to 8 items, and serializes it as a JSON object with keys "Item1", "Item2", etc.
- Discriminated unions, struct tuples: not supported.
FSharp.SystemTextJson takes over the serialization of these types by default; but the option types: JsonFSharpTypes
allows customizing which types should be serialized by FSharp.SystemTextJson
, and which types should be left to System.Text.Json
.
let options = JsonSerializerOptions()
// Only use FSharp.SystemTextJson for records and unions:
options.Converters.Add(JsonFSharpOptions(types = (JsonFSharpTypes.Records ||| JsonFSharpTypes.Unions)))
JsonSerializer.Serialize(Map [(1, "one"); (2, "two")], options)
// => {"1":"one","2":"two"}
// whereas FSharp.SystemTextJson would have serialized as:
// => [[1,"one"],[2,"two"]]
Happy coding!
By Loïc "Tarmil" Denuzière on Sunday, September 25, 2022
fsharp release library fsharp-systemtextjson Tweet Permalink