[Fork] [Fork] https://stackoverflow.com/questions/54169707/f-internal-visibility-changes-record-constructor-behavior

FSharp · Script

/Program.fs

open System
open System.Collections
open System.Collections.Generic
open System.Linq
open System.Text
open System.Runtime.Serialization.Formatters
open System.IO
open System.Data
open System.ComponentModel.DataAnnotations
open System.Globalization
open System.Reflection
open Microsoft.FSharp.Reflection

open Newtonsoft.Json
open Newtonsoft.Json.Linq
open Newtonsoft.Json.Converters
open Newtonsoft.Json.Serialization

open NUnit.Framework

printfn "Environment version %O" System.Environment.Version
printfn "%s version: %O" (typeof<FSharpTypeFunc>.Namespace) (typeof<FSharpTypeFunc>.Assembly.GetName())
printfn "%s version: %O" (typeof<JsonConvert>.Namespace) (typeof<JsonConvert>.Assembly.FullName)
printfn "%s version: %O" (typeof<NUnit.Framework.Assert>.Namespace) (typeof<NUnit.Framework.Assert>.Assembly.GetName())
//https://dotnetfiddle.net/ crashes when attempting to link in Http.Flurl
//printfn "%s version: %O" (typeof<Flurl.Http.FlurlHttp>.Namespace) (typeof<Flurl.Http.FlurlHttp>.Assembly.GetName())

printfn ""

// https://stackoverflow.com/questions/54169707/f-internal-visibility-changes-record-constructor-behavior
// https://stackoverflow.com/questions/58702479/cant-serialize-records-in-f-with-json-net

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

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

open models

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)

printfn "JSON for %s is %s" (t.GetType().Name) json
Assert.AreEqual(t, t2)

printfn ""
printfn ""

type private test =
    {
        [<Newtonsoft.Json.JsonProperty>] a : string
        b : string list
    }

let private test = { a = "hello"; b = ["1"; "2"] }
Assert.AreEqual(test, { a = "hello"; b = ["1"; "2"] }) // Assert that NUnit works as expected

let testjson = JsonConvert.SerializeObject(test, settings)
let private testback = JsonConvert.DeserializeObject<test>(testjson, settings)
let testjsonback = JsonConvert.SerializeObject(testback, settings)

printfn "JSON for %s is %s" (test.GetType().Name) testjson

Assert.AreEqual(testjson, testjsonback)
Assert.AreEqual(test, testback)

printfn ""
printfn ""

[<Newtonsoft.Json.JsonObject(MemberSerialization = MemberSerialization.OptIn)>]
type private test2 =
    {
        [<Newtonsoft.Json.JsonProperty>]
        Included : string
        //[<Newtonsoft.Json.JsonProperty>]
        NotIncluded : string
    }

let private test2 = { Included = "hello"; NotIncluded = "Should not be present" }

let test2json = JsonConvert.SerializeObject(test2, settings)
let private test2back = JsonConvert.DeserializeObject<test2>(test2json, settings)
let test2json2 = JsonConvert.SerializeObject(test2back, settings)

printfn "JSON for %s is %s" (test2.GetType().Name) test2json
printfn "Round-trip JSON for %s is %s" (test2back.GetType().Name) test2json2

Assert.AreEqual(test2json, "{\"Included\":\"hello\"}")
Assert.AreEqual(test2json2, "{\"Included\":\"hello\"}")
Assert.AreNotEqual(test2, test2back)

#if false
// https://flurl.dev/
// https://github.com/tmenier/Flurl/tree/Flurl.Http.3.2.4/src/Flurl.Http/Configuration
// https://github.com/tmenier/Flurl/blob/Flurl.Http.3.2.4/src/Flurl.Http/Configuration/NewtonsoftJsonSerializer.cs
// https://flurl.dev/docs/configuration/#serializers
// https://github.com/tmenier/Flurl/issues/49
// https://www.nuget.org/packages/Flurl.Http
open Flurl.Http

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