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:
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.
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.