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.

FSharp.SystemTextJson 1.0 released!

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

FSharp.Data.LiteralProviders 1.0 is here!

I am happy to announce that the library FSharp.Data.LiteralProviders has reached version 1.0!

FSharp.Data.LiteralProviders is an F# type provider library that provides compile-time constants from various sources, such as environment variables or files:

open FSharp.Data.LiteralProviders

// Get a value from an environment variable, another one from a file,
// and pass them to another type provider.

let [<Literal>] ConnectionString = Env<"CONNECTION_STRING">.Value
let [<Literal>] GetUserDataQuery = TextFile<"GetUserData.sql">.Text

type GetUserData = FSharp.Data.SqlCommandProvider<GetUserDataQuery, ConnectionString>

let getUserData (userId: System.Guid) =
    GetUserData.Create().Execute(UserId = userId)

Here is a summary of the new features in v1.0.

Running an external command

The Exec provider runs an external command during compilation and provides its output.

open FSharp.Data.LiteralProviders

let [<Literal>] Branch = Exec<"git", "branch --show-current">.Output

More options are available to pass input, get the error output, the exit code, etc. See the documentation.

Conditionals

The sub-namespaces String, Int and Bool provide a collection of compile-time conditional operators for the corresponding types.

For example, you can compare two integer values with Int.LT; combine two booleans with Bool.OR; or choose between two strings with String.IF.

open FSharp.Data.LiteralProviders

// Compute the version: get the latest git tag, and add the branch if it's not master or main.

let [<Literal>] TagVersion = Exec<"git", "describe --tags">.Output

let [<Literal>] Branch = Exec<"git", "branch --show-current">.Output

// Note: the `const` keyword is an F# language quirk, necessary when nesting type providers.
let [<Literal>] IsMainBranch =
    Bool.OR<
        const(String.EQ<Branch, "master">.Value),
        const(String.EQ<Branch, "main">.Value)
    >.Value

let [<Literal>] Version =
    String.IF<IsMainBranch,
        Then = TagVersion,
        Else = const(TagVersion + "-" + Branch)
    >.Value

See the documentation for all the operators available.

Value as int and as bool

The providers try to parse string values as integer and as boolean. If any of these succeed, a value suffixed with AsInt or AsBool is provided.

open FSharp.Data.LiteralProviders

let [<Literal>] runNumberAsString = Env<"GITHUB_RUN_NUMBER">.Value // eg. "42"

let [<Literal>] runNumberAsInt = Env<"GITHUB_RUN_NUMBER">.ValueAsInt // eg. 42

By Loïc "Tarmil" Denuzière on Friday, May 27, 2022

fsharp release library literalproviders Tweet Permalink