Home > Enterprise >  How to define SingleOrArrayConverter to Handle JSON property in F# ( Port from C# )
How to define SingleOrArrayConverter to Handle JSON property in F# ( Port from C# )

Time:09-01

I am porting some ( well tested over time ) code from C# to F# and having some issues getting something to work in F#

The C# Code:

( Object I want to serialise )

public class Locality
{
    public string category { get; set; }
    public int id { get; set; }
    public string location { get; set; }
    public string postcode { get; set; }
    public string state { get; set; }
    public double? latitude { get; set; }
    public double? longitude { get; set; }
}
public class Localities
{
    [JsonProperty("locality")]
    [JsonConverter(typeof(R2H.Models.JSon.SingleOrArrayConverter<Locality>))]
    public List<Locality> locality { get; set; }
}

public class AuspostPostCodeLocality
{
    public Localities localities { get; set; }
}

( JSON converter )

public class SingleOrArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T> { token.ToObject<T>() };
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

( Attempt at F# Code )

type SingleOrArrayConverter<'T>() =
    inherit JsonConverter()
    override this.CanConvert(objectType : Type) = 
        objectType = typeof<List<'T>>
    override this.ReadJson(reader : JsonReader, objectType : Type, existingValue : System.Object, serializer : JsonSerializer) = 
        let mutable (token : JToken) = JToken.Load (reader)
        if token.Type = JTokenType.Array then
            (token.ToObject<List<'T>> ())
        else
            ([|(token.ToObject<'T> ())|])
    override this.CanWrite
        with get() = 
            false
    override this.WriteJson(writer : JsonWriter, value : System.Object, serializer : JsonSerializer) = 
        raise (new NotImplementedException() :> System.Exception)

And my attempt at the model ( You can see several attempts commented out ).

type Locality = {
    category: string
    id: int
    location: string
    postcode: int
    state: string
    latitude: decimal
    longitude: decimal
}
type Localities = {
    //inherit JsonConverter<SingleOrArrayConverter<Locality>>()
    //[<JsonProperty("locality");JsonConverter<SingleOrArrayConverter<Locality>>>]
    //[<JsonConverter(typeof (SingleOrArrayConverter<Locality>))>]
    //[<JsonProperty("locality")>]
    locality: List<Locality>
}
type PostCodeLocality = {
    localities : Localities
}

CodePudding user response:

Since your converter seems to have originated from this answer by Brian Rogers to How to handle both a single item and an array for the same property using JSON.net, I assume that you are trying to create a generic converter for mutable lists of type System.Collections.Generic.List<'T> (abbreviated to ResizeArray in FSharpx.Collections).

F# also has an immutable list type FSharp.Collections.List<'T> abbreviated to list. Be sure which one you want to use.[1]

With that in mind, assuming you want System.Collections.Generic.List<'T> your converter can be written as follows:

type SingleOrArrayConverter<'T>() =
    inherit JsonConverter()
    override this.CanConvert(objectType) = objectType = typeof<ResizeArray<'T>> 
    override this.ReadJson(reader, objectType, existingValue, serializer) =  // Unlike in C# it's not necessary to declare the types of the arguments when there is no ambiguity
        let token = JToken.Load (reader)
        if token.Type = JTokenType.Array then
            // Upcast to obj as upcasting is not automatic for returned value
            // https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/casting-and-conversions#upcasting
            (token.ToObject<ResizeArray<'T>> ()) :> obj
        else
            ResizeArray [|(token.ToObject<'T> ())|] :> obj  // Convert array to List<T> then upcast to object
    override this.CanWrite = false                       // Simplified syntax
    override this.WriteJson(writer, value, serializer) = raise (new NotImplementedException())

And your Localities type defined as follows:

type Localities = {
    [<JsonProperty("locality")>]
    [<JsonConverter(typeof<SingleOrArrayConverter<Locality>>)>]  // Fix syntax: typeof<'T> not typeof(T)
    locality: ResizeArray<Locality>  // Be sure whether you want System.Collections.Generic.List<'T> a.k.a ResizeArray<'T> or FSharp.Collections.List<'T>
}

Demo fiddle #1 here.

If you do want to use f#'s immutable list, define your converter as follows:

type SingleOrArrayFSharpListConverter<'T>() =
    inherit JsonConverter()
    override this.CanConvert(objectType) = objectType = typeof<list<'T>>
    override this.ReadJson(reader, objectType, existingValue, serializer) =  // Unlike in C# it's not necessary to declare the types of the arguments when there is no ambiguity
        let token = JToken.Load (reader)
        if token.Type = JTokenType.Array then
            // Upcast to obj as upcasting is not automatic for returned value
            // https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/casting-and-conversions#upcasting
            (token.ToObject<list<'T>> ()) :> obj
        else
            [token.ToObject<'T>()] :> obj  // Convert array to list<T> then upcast to object
    override this.CanWrite = false                       // Simplified syntax
    override this.WriteJson(writer, value, serializer) = raise (new NotImplementedException())

And modify Localities as follows:

type Localities = {
    [<JsonProperty("locality")>]
    [<JsonConverter(typeof<SingleOrArrayFSharpListConverter<Locality>>)>]  // Fix syntax: typeof<'T> not typeof(T)
    locality: Locality list  // Be sure whether you want System.Collections.Generic.List<'T> a.k.a ResizeArray<'T> or FSharp.Collections.List<'T>
}

Demo fiddle #2 here.


[1] For details see Creating a generic List <T> in F#.

  • Related