FSharp · Script
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