Serializing F# Discriminated union as strings with Json.NET
Asked Answered
S

3

9

I'm trying to do a one way transform from F#'s discriminated union to strings upon serialization instead of the default `"Case": [value]". Being able to deserialize the value again is not an issue. Maybe possible with Json.NET?

// Fsharp 4.1.0
open Newtonsoft.Json // 10.0.3

type HowLame =
| PrettyLame
| SuperLame

type Lame = {
    howLame: HowLame;
}

[<EntryPoint>]
let main argv =
    let lame = { howLame = PrettyLame }
    let ser = JsonConvert.SerializeObject(lame)

    // {"soLame":{"Case":"PrettyLame"}} by default
    printfn "%s" ser

    // Desired
    assert (ser = """{"soLame":"PrettyLame"}""")
    0 // return an integer exit code
Stagestruck answered 12/11, 2017 at 9:31 Comment(0)
E
7

If you are willing to make the DU an enum (by specifying explicit values, which probably is OK since there is no 'payload'), you can use the standard StringEnumConverter:

#r "../packages/Newtonsoft.Json/lib/net45/Newtonsoft.Json.dll"
open Newtonsoft.Json

type HowLame = PrettyLame=0 | SuperLame=1
type Lame = { howLame: HowLame; }

// in contrast to DUs, enums must be qualified, i.e. Enum.Value
let lame = { howLame = HowLame.PrettyLame }

let settings = JsonSerializerSettings()
settings.Converters.Add(Converters.StringEnumConverter())

let ser = JsonConvert.SerializeObject(lame, settings)
// val ser : string = "{"howLame":"PrettyLame"}"
Evelineevelinn answered 13/11, 2017 at 1:40 Comment(1)
In addition, have a look at Json.FSharp, especially the UnionConverter and maybe ChironEvelineevelinn
S
9

Creating a custom Json.NET JsonConverter and using it to decorate the discriminated union ("enum style") was enough to get this working the way I wanted. A good chunk of this is transliterated from @Brian Rogers answer in C# https://mcmap.net/q/219215/-how-to-make-json-net-serializer-to-call-tostring-when-serializing-a-particular-type

open System
open Newtonsoft.Json // 10.0.3
open Newtonsoft.Json.Converters

type ToStringJsonConverter () =
    inherit JsonConverter()
    override this.CanConvert objectType = true;

    override this.WriteJson (writer: JsonWriter, value: obj, serializer: JsonSerializer): unit = 
        writer.WriteValue(value.ToString())

    override this.CanRead = false

    override this.ReadJson (reader: JsonReader, objectType: Type, existingValue: obj, serializer: JsonSerializer) : obj =
        raise (new NotImplementedException());

[<JsonConverter(typeof<ToStringJsonConverter>)>]
type HowLame =
| PrettyLame
| SuperLame

type Lame = {
    howLame: HowLame
}

[<EntryPoint>]
let main argv =
    let lame = { howLame = PrettyLame }
    let ser = JsonConvert.SerializeObject(lame)

    // {"howLame":"PrettyLame"}
    printfn "%s" ser

    0 // return an integer exit code
Stagestruck answered 12/11, 2017 at 12:45 Comment(0)
E
7

If you are willing to make the DU an enum (by specifying explicit values, which probably is OK since there is no 'payload'), you can use the standard StringEnumConverter:

#r "../packages/Newtonsoft.Json/lib/net45/Newtonsoft.Json.dll"
open Newtonsoft.Json

type HowLame = PrettyLame=0 | SuperLame=1
type Lame = { howLame: HowLame; }

// in contrast to DUs, enums must be qualified, i.e. Enum.Value
let lame = { howLame = HowLame.PrettyLame }

let settings = JsonSerializerSettings()
settings.Converters.Add(Converters.StringEnumConverter())

let ser = JsonConvert.SerializeObject(lame, settings)
// val ser : string = "{"howLame":"PrettyLame"}"
Evelineevelinn answered 13/11, 2017 at 1:40 Comment(1)
In addition, have a look at Json.FSharp, especially the UnionConverter and maybe ChironEvelineevelinn
U
3

Now in 2024, I am using:

https://github.com/fsprojects/FSharp.Json

It uses System.Text.Json and supports DUs nicely. I configured it like this:

namespace Sample.Json

open System.Text.Json
open System.Text.Json.Serialization

module Json =

    let jsonOptions = JsonSerializerOptions()
    jsonOptions.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull

    JsonFSharpConverter(
        unionEncoding =
            (JsonUnionEncoding.InternalTag
             ||| JsonUnionEncoding.NamedFields
             ||| JsonUnionEncoding.UnwrapOption
             ||| JsonUnionEncoding.AllowUnorderedTag

            ),
        allowOverride = true,
        unionTagCaseInsensitive = false
    )
    |> jsonOptions.Converters.Add


    let asJson (obj: 'a) =
        JsonSerializer.Serialize(obj, jsonOptions)

    let asJsonDocument (obj: 'a) : JsonDocument =
        JsonSerializer.Serialize(obj, jsonOptions) |> JsonDocument.Parse

    let fromJson<'a> (json: string) =
        JsonSerializer.Deserialize<'a>(json, jsonOptions)

The downside is that for some scenarios, like Azure Functions, you can't bind to an F# DU directly. Therefore, I need to convert the object to a JsonDocument and after, convert back and forth using the utility functions above.

On Cosmos, this is a example of the result for a single case DU

type CommandState = 
        | Created of DateTimeOffset
        | Completed of DateTimeOffset
        | Failed of failureDate: DateTimeOffset * errorMessage: string * callStack: string option
 
 ...
 "state": {
        "Case": "Created",
        "Item": "2024-01-10T16:39:19.3992788-03:00"
    },
 ...

When the field has a name it also uses the name in the json. For me it's working well so far. I hope this helps!

Underlinen answered 10/1 at 21:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.