Tarmil / FSharp.SystemTextJson

System.Text.Json extensions for F# types
MIT License
323 stars 44 forks source link

Support getting JsonFSharpOptions and a list of overrides from JsonFSharpConverter #112

Closed AlexeyRaga closed 2 years ago

AlexeyRaga commented 2 years ago

This is useful for being able to "extend" an existing JsonFSharpConverter in a way that is "just like the existing one, but altered".

It can be used from within other JsonConverters, when they decide that parts of the format should be altered.

Use case:

I have a type

type Envelope<'a> =
    { id : int
      headers : Map<string, string>
      data : 'a }

in which 'a can be an F# type, often a union type. I am writing a JsonConverter (the factory) to be able to format the value according to some standard, like:

{
    "id": 1,
    "headers":
    {
        "fizz": "buzz",
        "foo": "bar"
    },
    "messageType": "Events.Order.Cancelled",
    "data":
    {
        "id": 123,
        "reason": "just"
    }
}

Note that in this case messageType sits outside of the data, so data has to be formatted as JsonUnionEncoding.ExternalTag ||| JsonUnionEncoding.NamedFields, regardless of how the formatter is configured otherwise. So I want to override this for the 'a within that envelope.

The logical case here seems to be to add an override for the type within the factory. But I wanted to preserve other overrides that might be configured, since that 'a can internally contain any of these.

I tried to add an override for typedefof<Envelope<_>> at the top level, but it obviously didn't work since It means providing options for the Envelope itself and not for the 'a inside it.

If JsonFSharpConverter provided the way of extracting options and overrides then it would be possible to do something like (pseudocode):

override this.CreateConverter(typeToConvert, options) =
    let fsConverter = options.Converters |> findFsharpConverter // returns existing one or default
    let newConverter = JsonFSharpOptions(fsConverter.options, fsConverter.overrides |> updatedOverridesForMyType)

    let safeOptions = JsonSerializerOptions(options)
    safeOptions.Converters |> replaceFsharpConverter newConverter

    // now use safeOptions instead of options

Ideally it'd be great to be able to declare an override for generic parameters, like "for the parameter within this generic use this override", but I don't even know how to express that :)))

AlexeyRaga commented 2 years ago

I think one alternative would be to have JsonFSharpConverterAttribute to be allowed on fields like:

type Envelope<'a> =
    { id : int
      headers : Map<string, string>
      [<JsonFSharpConverter(unionEncoding = JsonUnionEncoding.Untagged)>]
      data : 'a }

but I think that it'd require much more work than exposing fsOptions and overrides as public properties...

Tarmil commented 2 years ago

Sure, making these two properties public sounds reasonable.

Tarmil commented 2 years ago

Done in v0.19.