fsprojects / Fleece

Json mapper for F#
http://fsprojects.github.io/Fleece
Apache License 2.0
199 stars 31 forks source link

Different json representation of Map<> depending on codec definition #147

Closed DunetsNM closed 4 months ago

DunetsNM commented 4 months ago

Consider following code, where I have three types with slightly different styles of Map field codec

open Fleece
open Fleece.SystemTextJson

// Wrapper types, identical except style of the codec
type MapWrap1 = { M: Map<string, int> }
with
    static member get_Codec() =
        codec {
            let! m = jreqWith (Codecs.map Codecs.string Codecs.int) "M" (fun x -> Some x.M)
            return ({ M = m } : MapWrap1)
        }
        |> ofObjCodec

type MapWrap2 = { M: Map<string, int> }
with
    static member get_Codec() =
        codec {
            let! m = jreqWith defaultCodec<_, Map<string, int>> "M" (fun x -> Some x.M)
            return ({ M = m } : MapWrap2)
        }
        |> ofObjCodec

type MapWrap3 = { M: Map<string, int> }
with
    static member get_Codec() =
        codec {
            let! m = Fleece.Operators.jreq "M" (fun x -> Some x.M)
            return ({ M = m } : MapWrap3)
        }
        |> ofObjCodec

let theMap = Map.ofList [("a", 3); ("b", 4); ("c", 5)]

// three different wrappers of the same map
let m1: MapWrap1 = { M = theMap }
let m2: MapWrap2 = { M = theMap }
let m3: MapWrap3 = { M = theMap }

// json has to be the same, right?
printfn "%s" (toJsonText m1)
printfn "%s" (toJsonText m2)
printfn "%s" (toJsonText m3)

I would expect it to print three identical json lines. However this is what it actually prints:

{"M":[["a",3],["b",4],["c",5]]}
{"M":{"a":3,"b":4,"c":5}}
{"M":{"a":3,"b":4,"c":5}}

Apparently Codecs.map Codecs.string Codecs.int and defaultCodec<_, Map<string, MyInterface>> (or its implicit equivalent jreq) produce different json output: in first case it's a list of key-value tuples, while in the second case it's a JSON object.

Is it a bug or by design? If a bug then there's no simple fix given that changing behaviour for either of the codecs is a breaking change. Perhaps only obsoleting Codecs.map and introducing two different functions instead of it ?

gusty commented 4 months ago

This is by-design.

Actually the default codec for maps where the key is a string is Codecs.propMap, change:

let! m = jreqWith (Codecs.map Codecs.string Codecs.int) "M" (fun x -> Some x.M)

for

let! m = jreqWith (Codecs.propMap Codecs.int) "M" (fun x -> Some x.M)

and you'll get all 3 representations identical.

In fact, in previous versions of Fleece there was no codec for "generic" maps, this was added in this latest version and the default was special-cased for the case where the keys are strings.

Maps codecs who's keys are string were always used as a key component of the library which is the map representation of a json object, where the strings are the names of the properties and the value is the IEncoding. As said above, this is called propMap.

DunetsNM commented 4 months ago

default codec for maps where the key is a string is Codecs.propMap

hmm, that's interesting. So for other types of keys it is just Codecs.map, right? Anyway, closing since it is by design.

gusty commented 4 months ago

Yes, the default for non string ones is the generic. It is by design, however if you feel like it's not a good design and have a better alternative we're all ears.