2

I'm caling an API using Flurl.

//# models.fs
module models =

    type Ticker = { Ask :decimal; Bid :decimal; Last: decimal; High :decimal; Timestamp :int; } 

//# Client.fs
namespace MyLibrary
// ... some code
    url.GetJsonAsync<models.Ticker>()

This works and I can access the ticker.Ask property.

The class models.Ticker is visible from another C# project and the construcor is this:

public Ticker(decimal ask, decimal bid, decimal last, decimal high, int timestamp);

I don't want expose the models module and the Ticker class/record so I changed the visibility to internal:

# models.fs
module internal models =

    type Ticker = { Ask :decimal; Bid :decimal; Last: decimal; High :decimal; Timestamp :int; } 

The code still "compile" but when I run it I have this exception:

  • Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type MyProject.models+Ticker. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'high', line 1, position 8. at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject(JsonReader reader, JsonObjectContract objectContract, JsonProperty containerMember, JsonProperty containerProperty, String id, Boolean& createdFromNonDefaultCreator) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonSerializer.Deserialize[T](JsonReader reader) at Flurl.Http.Configuration.NewtonsoftJsonSerializer.Deserialize[T](Stream stream) at Flurl.Http.HttpResponseMessageExtensions.ReceiveJson[T](Task`1 response)} System.Exception {Newtonsoft.Json.JsonSerializationException}*

enter image description here

This is the full code example, I'm new with F#, maybe needed to explain the problem:

open Flurl.Http
// open mynamespace where is models

type IClient =
    abstract member GetAsk : decimal   

type MyClient() =

    let getTicker () = 
        let url = "https://www.bitstamp.net/api/v2/ticker/XRPUSD"
        url.GetJsonAsync<models.Ticker>()                

    interface IClient with
        member  __.GetAsk =         
            let ticker = getTicker().Result
            ticker.Ask 

I'm using the Client from a C# project. I imagine the constructor of Ticker changed because of the visibility. But, why? How can I maintain the Ticker class hidden and make the code works as expected?

[Update: use a class]
Using a class

type internal Ticker(ask:decimal, bid :decimal, last: decimal, high :decimal, timestamp :int) =
    member this.Ask = ask    

does the job as expected. If internal it is not visible externally but it is "usable" by the JsonDeserializer.
I'm still confused for the behavior when I use a Record.

Todd Menier
  • 37,557
  • 17
  • 150
  • 173
Alex 75
  • 2,798
  • 1
  • 31
  • 48
  • 1
    Since you made the constructor inaccessible, the JSON deserializer cannot access it. Works exactly as it's supposed to. – Fyodor Soikin Jan 13 '19 at 14:38
  • Are you allowed to modify the Ticker class? – Todd Menier Jan 13 '19 at 15:22
  • Thanks. I use "internal" to make the Ticker Record/class visible to the Client (same library) but not visible to external consumers of the library (other F# or C# projects). Infact it is "visible" to the Client, the intellisense confirms this. The library use it internally and return the Ask property (decimal), not the Ticker. It is job of the Flurl to parse the JSON into the class. At thist point, inside the library, the Ticker is visible.Where I'm wrong? What I'm missing? – Alex 75 Jan 13 '19 at 19:35
  • Yes Todd. The Ticker class (Record) is mine and it is inside the F# library, I created it with the sole purpose to parse the API result but I don't want to expose it externally. I made a mistake with the namespace ??? – Alex 75 Jan 13 '19 at 19:37
  • Current code: Commit [d6f51698](https://alex75.visualstudio.com/_git/Bitstamp%20API%20Client/commit/d6f51698dc14e3bdde1583cf1a7d557d629d1d37?refName=refs%2Fheads%2Fmaster) – Alex 75 Jan 13 '19 at 19:54
  • 3
    The record is visible inside _your_ library, but it is not visible inside any external libraries, one is which is the JSON deserializer. – Fyodor Soikin Jan 13 '19 at 21:18
  • That was not the expected behavior just changing the visibility. To solve this, maintain Ticker not visible outside, I have to use a class instead of a record, right? – Alex 75 Jan 14 '19 at 08:11
  • That was not the expected behavior just changing the visibility. I mean the fact that the constructor is visible or not (that is the real difference I can see), because inside GetAsk I can _see_ Ticker and the deserializer can see it too, it is only the absence of the constructor the difference. To solve this, maintain Ticker not visible outside, I have to use a class instead of a record, right? or there is an Attribute that can do the job (I'll look for this)? – Alex 75 Jan 14 '19 at 08:18
  • The root of the problem seems to be described in [Fields and methods on internal types are impossible to make public #2820](https://github.com/dotnet/fsharp/issues/2820#issuecomment-386664919). In f# a non-public type cannot have public properties or methods -- even when declared `public` they are coerced to be nonpublic. Thus Json.NET does not know to use them. – dbc Jun 29 '19 at 08:57

1 Answers1

1

Your problem is that essentially all .NET serializers including Json.NET, XmlSerializer, System.Text.Json and so on are designed to serialize the public members of types -- even if the type itself is non-public 1. However, as explained in the F# documentation Rules for Access Control:

  • Accessibility for individual fields of a record type is determined by the accessibility of the record itself. That is, a particular record label is no less accessible than the record itself.

Thus the fields of an internal record are not public. And there is no way to make them public according to the issue Fields and methods on internal types are impossible to make public #2820.

To work around this, you are going to need to create a custom contract resolver that, for any nonpublic F# record type, considers all record fields to be public. In addition it will be necessary to invoke the constructor manually because the constructor will also have been marked as nonpublic.

The following contract resolver does the job:

type RecordContractResolver() =
    inherit DefaultContractResolver()    
    
    let IsPrivateRecordType(t : Type) = 
        (not t.IsPublic && FSharpType.IsRecord(t, BindingFlags.NonPublic))
    let CreateParameterizedConstructor(c : ConstructorInfo) = 
        ObjectConstructor<Object>(fun a -> c.Invoke(a))  // There's no easy way to manufacture a delegate from a constructor without Reflection.Emit, just call Invoke()

    override this.GetSerializableMembers(objectType : Type) = 
        // F# declares nonpublic record members to be nonpublic themselves, so Json.NET won't discover them.  Fix that by adding them in.
        let members = base.GetSerializableMembers(objectType)
        if IsPrivateRecordType(objectType) then
            let baseMembers = HashSet members // Some members might already be included if marked with [JsonProperty]
            let addedMembers = 
                FSharpType.GetRecordFields(objectType, BindingFlags.Instance ||| BindingFlags.NonPublic)
                |> Seq.filter (fun p -> p.CanRead && p.GetIndexParameters().Length = 0 && not (baseMembers.Contains(p)) && not (Attribute.IsDefined(p, typeof<JsonIgnoreAttribute>)))
            for p in addedMembers do
                members.Add(p)
        members

    override this.CreateProperty(m : MemberInfo, memberSerialization : MemberSerialization) =
        // Even though F# record members are readable, Json.NET marks them as not readable because they're not public.  Fix that.
        let property = base.CreateProperty(m, memberSerialization)
        if (IsPrivateRecordType(m.DeclaringType)
            && not property.Readable
            && m :? PropertyInfo
            && FSharpType.GetRecordFields(m.DeclaringType, BindingFlags.Instance ||| BindingFlags.NonPublic).Contains(m :?> PropertyInfo)) then
            property.Readable <- true
        property

    override this.CreateObjectContract(objectType : Type) = 
        // Pick the constructor with the longest argument list.
        // Adapted from this answer https://stackoverflow.com/a/35865022 By Zoltán Tamási https://stackoverflow.com/users/1323504/zolt%c3%a1n-tam%c3%a1si
        // To https://stackoverflow.com/questions/23017716/json-net-how-to-deserialize-without-using-the-default-constructor
        let contract = base.CreateObjectContract(objectType)
        if IsPrivateRecordType(objectType) then
            let constructors = objectType.GetConstructors(BindingFlags.Instance ||| BindingFlags.Public ||| BindingFlags.NonPublic) |> Seq.sortBy (fun c -> c.GetParameters().Length)
            let c = constructors.LastOrDefault()
            if not (Object.ReferenceEquals(c, null)) then
                contract.OverrideCreator <- CreateParameterizedConstructor(c)
                for p in (this.CreateConstructorParameters(c, contract.Properties)) do
                    contract.CreatorParameters.Add(p)
        contract    

Then to manually serialize and deserialize models.Ticker, you would set up your JsonSerializerSettings as follow:

let internal t = {Ask = 0.7833m; Bid = 0.7833m; Last = 0.7833m; High = 0.7900m; Timestamp  = 1010101} 

let settings = new JsonSerializerSettings()
settings.ContractResolver <- new RecordContractResolver()

let json = JsonConvert.SerializeObject(t, settings)
let internal t2 = JsonConvert.DeserializeObject<models.Ticker>(json, settings)

And to configure Flurl.Http with custom settings see their documentation page Configuration : Serializers, which, when translated to F#, should look something like:

Flurl.Http.FlurlHttp.Configure(fun s -> 
    let settings = new JsonSerializerSettings()
    settings.ContractResolver <- new RecordContractResolver()
    s.JsonSerializer <- NewtonsoftJsonSerializer settings
    )

Notes:

  • RecordContractResolver only modifies the serialization of nonpublic F# record types. Contracts for all other types are unchanged.

  • Newtonsoft recommends to cache contract resolvers for best performance.

  • I tested the contract resolver standalone, but could not test it with Flurl. The Flurl code was written against Flurl.Http version 3.2.4 which is the latest released version at the time of writing this answer.

  • Flurl.Http 4.0 is set to replace Json.NET with System.Text.Json so you will have the same problem all over again if you upgrade. And unfortunately System.Text.Json's more limited contract customization in .NET 7 does not allow for customization of parameterized constructors, so it may prove even more difficult to deserialize internal F# records in that release.

Demo fiddle here.


1 One notable exception to this is the deprecated BinaryFormatter which serializes all fields whether public or private. However, according to the docs:

BinaryFormatter serialization is obsolete and should not be used.

dbc
  • 104,963
  • 20
  • 228
  • 340