Zaid-Ajaj / Fable.Remoting

Type-safe communication layer (RPC-style) for F# featuring Fable and .NET Apps
https://zaid-ajaj.github.io/Fable.Remoting/
MIT License
272 stars 54 forks source link

Union type and CamelCasePropertyNamesContractorResolver #220

Closed kspeakman closed 3 years ago

kspeakman commented 3 years ago

I'm using Fable.Remoting.Json stand-alone. I wanted to emit JSON as camel case. I tried using the CamelCasePropertyNamesContractorResolver that is built into Newtonsoft, but union types which are emitted as properties still come out Pascal cased.

namespace Utilities.Json

module CamelCaselJson =

    open Newtonsoft.Json
    open Newtonsoft.Json.Serialization
    open Fable.Remoting.Json

    let contractResolver = new CamelCasePropertyNamesContractResolver()

    let Settings =
        let s = new JsonSerializerSettings()
        s.ContractResolver <- contractResolver
        s.Converters.Add(new FableJsonConverter())
        s.NullValueHandling <- NullValueHandling.Ignore
        // https://github.com/Zaid-Ajaj/Fable.Remoting/pull/110
        s.DateParseHandling <- DateParseHandling.None
        s

    let serialize a = JsonConvert.SerializeObject(a, Settings)

Example output:

type TestError =
    { Code: string }

CamelCaseJson.serialize (Error { Code = "Foo" })
// output: { "Error": { "code": "Foo" } }
// expected: { "error": { "code": "Foo" } }

If compatibility with CamelCasePropertyNamesResolver is not possible/desired, is there a setting on FableJsonConverter to make this happen? For my case, adding serialization-specific attributes to types or properties creates a tricky dependency for shared libraries, so it is not feasible. Plus, Result is a built-in type so I cannot add attributes to it.

Thanks for your time and this library.

Zaid-Ajaj commented 3 years ago

Hi there @kspeakman, I don't think FableJsonConverter looks at the resolver at all, it just does everything as default. Surely, it is possible to extend FableJsonConverter with more options:

settings.Converters.Add(FableJsonConverter(camelCase=true))

but the implementation would take a considerable amount of work (I think). Personally, I would rather not touch Fable.Remoting.Json when possible because it bring about a rabbit hole of edge cases across the stack. For your use-case, maybe try out the auto encoders and decoders from Thoth where AFAIK they do have a camel casing option. Alternatively, FSharp.SystemTextJson also has these types of customizations. Please try those out first and if nothing works, we can try figure something out from here 😄

kspeakman commented 3 years ago

I understand. I need to re-evaluate STJ to see how far it has come along since I last looked at it. For brevity I omitted some things that I also use from Newtonsoft which I would need to replicate in STJ. For example, this makes Option types semantically mean that fields are optional and non-Option fields are required.

    type RequireAllPropertiesContractResolver() =
        inherit CamelCasePropertyNamesContractResolver()

        override __.CreateProperty(memberInfo:MemberInfo, memberSerialization:MemberSerialization) =
            let property = base.CreateProperty(memberInfo, memberSerialization)
            let t =
                match memberInfo with
                | :? PropertyInfo as pi -> pi.PropertyType
                | :? FieldInfo as fi -> fi.FieldType
                | _ -> typeof<obj>

            // only option types can be, well, optional
            if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<Option<_>> then
                property.Required <- Required.Default
            else
                property.Required <- Required.Always
            property

Anyway, I will see what I can do with STJ. I will eventually move to it for performance reasons anyway. If nothing else, I can go back to my own custom F# converters I had pieced together before I was using Fable.Remoting.Json.