Search code examples
f#json.nettypeconverterdiscriminated-unionjsonconvert

Why does an F# Discriminated Union fails to have its TypeConverter respected by JSON.NET but other types can?


#r "../packages/Newtonsoft.Json.12.0.3/lib/netstandard2.0/Newtonsoft.Json.dll"
type [<Struct; System.ComponentModel.TypeConverterAttribute(typeof<CC>)>] C = A of string
and CC() = 
    inherit System.ComponentModel.TypeConverter()
    override _.CanConvertFrom (_, t) = t = typeof<string>
    override _.ConvertFrom(_, _, s) = s :?> string |> A |> box<C>
    override _.CanConvertTo (_, t) = t = typeof<string>
    override _.ConvertTo(_, _, s, _) = s :?> C |> fun (A s) -> s |> box<string>
Newtonsoft.Json.JsonConvert.SerializeObject {|a = A "123"|}

This results in val it : string = "{"a":{"Case":"A","Fields":["123"]}}", which indicates that the TypeConverter is not respected. This also happens for reference DUs.

However, this does not happen with JsonConverters:

#r "../packages/Newtonsoft.Json.12.0.3/lib/netstandard2.0/Newtonsoft.Json.dll"
type [<Struct; Newtonsoft.Json.JsonConverter(typeof<CC>)>] C = A of string
and CC() = 
    inherit Newtonsoft.Json.JsonConverter()
    override _.CanConvert t = t = typeof<string>
    override _.ReadJson (r, _, _, _) = r.ReadAsString() |> A |> box<C>
    override _.WriteJson (w, v, _) = v :?> C |> fun (A s) -> s |> w.WriteValue
Newtonsoft.Json.JsonConvert.SerializeObject {|a = A "123"|}

This results in val it : string = "{"a":"123"}".

Compare this with records:

#r "../packages/Newtonsoft.Json.12.0.3/lib/netstandard2.0/Newtonsoft.Json.dll"
type [<Struct; System.ComponentModel.TypeConverterAttribute(typeof<CC>)>] C = { A: string }
and CC() = 
    inherit System.ComponentModel.TypeConverter()
    override _.CanConvertFrom (_, t) = t = typeof<string>
    override _.ConvertFrom(_, _, s) = { A = s :?> string } |> box<C>
    override _.CanConvertTo (_, t) = t = typeof<string>
    override _.ConvertTo(_, _, s, _) = (s :?> C).A |> box<string>
Newtonsoft.Json.JsonConvert.SerializeObject {|a = { A = "123"}|}

This also results in val it : string = "{"a":"123"}", which indicates that the TypeConverter is respected.

This shows that something is preventing TypeConverters in discriminated unions from being recognized. What would be the reason? JsonConverters are not usable in dictionary keys, so I would expect TypeConverters to perform better. What would be a viable approach to correctly serialize the aforementioned discriminated union?


Solution

  • Your problem is that Json.NET has its own built-in converter for discriminated unions, DiscriminatedUnionConverter. Any applicable JsonConverter will always supersede an applied TypeConverter.

    A built-in converter can be disabled by providing your own, alternate, JsonConverter, either in settings or via an applied JsonConverterAttribute. You have already created a converter that correctly converts your type C, but if you would prefer to fall back to the applied TypeConverter, you can create a JsonConverter that does nothing and falls back on default serialization by returning false from CanRead and CanWrite:

    type NoConverter<'a> () =
        inherit JsonConverter()
        override this.CanConvert(t) = (t = typedefof<'a>)
        override this.CanRead = false
        override this.CanWrite = false
        override this.WriteJson(_, _, _) =  raise (NotImplementedException());
        override this.ReadJson(_, _, _, _) =  raise (NotImplementedException());
    

    Then apply it to your type as follows (demo fiddle #1 here):

    type [<JsonConverterAttribute(typeof<NoConverter<C>>); System.ComponentModel.TypeConverterAttribute(typeof<CC>)>] C = A of string
    and CC() = 
        inherit System.ComponentModel.TypeConverter()
        override this.CanConvertFrom (_, t) = (t = typeof<string>)
        override this.ConvertFrom(_, _, s) = s :?> string |> A |> box<C>
        override this.CanConvertTo (_, t) = t = typeof<string>
        override this.ConvertTo(_, _, s, _) = s :?> C |> fun (A s) -> s |> box<string>
    
    printfn "%s" (Newtonsoft.Json.JsonConvert.SerializeObject(A "123"))
    

    Or, use it in settings as follows (demo fiddle #2 here):

    let settings = JsonSerializerSettings(Converters = [|NoConverter<C>()|])
    printfn "%s" (Newtonsoft.Json.JsonConvert.SerializeObject(A "123", settings))