daniellittledev / FSharp.Explicit.Json

An explicit JSON serialisation library based on System.Text.Json that uses explicit mappings to parse JSON
https://daniellittledev.github.io/FSharp.Explicit.Json/
MIT License
10 stars 1 forks source link

Integration into System.Text.Json #3

Open jpeg729 opened 2 years ago

jpeg729 commented 2 years ago

I really like the idea of validating the input during serialization, even if I am not entirely convinced that it is a good idea. So I tried imagining how I might integrate this into System.Text.Json

Here is the beginnings of an idea.


[<JsonConverter(typeof<WeatherForecastConverter>)>]
type WeatherForecast =
    { Date: DateTime
      TemperatureC: int
      Summary: string }

and WeatherForecastConverter() =
    inherit JsonConverter<WeatherForecast>()

    override _.Read(reader: byref<Utf8JsonReader>, typeToConvert: Type, options: JsonSerializerOptions) =
        let doc = JsonSerializer.Deserialize<JsonDocument>(&reader, options)

        let weather =
            doc
            |> Parse.document (fun node ->
                validation {
                    let! date = node.prop "date" Parse.string
                    and! tempC = node.prop "temperatureC" Parse.int
                    and! summary = node.prop "summary" Parse.string

                    let! date =
                        match DateTime.TryParse date with
                        | true, d -> Ok d
                        | false, _ ->
                            Error [ { JsonParserError.path = [ "date" ]
                                      reason = JsonParserErrorReason.UserError "bad date" } ]

                    return
                        { Date = date
                          TemperatureC = tempC
                          Summary = summary }
                })

        match weather with
        | Ok w -> w
        | Error e -> FormatException("bad date", Source = "System.Text.Json.Rethrowable") |> raise
        // if we throw a FormatException with Source = "System.Text.Json.Rethrowable"
        // then JsonSerializer catches it and throws a System.Text.Json.JsonException with
        //   Message =  'The JSON value could not be converted to FSharpExplicitJson.WeatherForecast. Path: $ | LineNumber: 4 | BytePositionInLine: 9.'
        //   InnerException = the FormatException

    override _.Write(writer: Utf8JsonWriter, value: WeatherForecast, options: JsonSerializerOptions) =
        let jsonWriter =
            Render.object {
                prop "date" (Render.datetime value.Date)
                prop "temperatureC" (Render.int value.TemperatureC)
                prop "summary" (Render.string value.Summary)
            }

        jsonWriter.Invoke writer
        ()

    override _.HandleNull = true // does this allow us to throw if we receive null instead of an object?

Remarks

WeatherForecast is a bag of primitive values, if it contained other complex types then Write could still work fine, though I would consider making use of Render(fun writer -> JsonSerializer.Serialize(value) for values of complex properties. Read as implemented naïvely here would require the instantiation of the entire object tree to take place here, since deserializing as JsonDocument won't instantiate any child types.

The use of the attribute is optional, you can attach the Converter to the JsonSerializationOptions object that you pass to System.Text.Json.

You seem to be working towards something that will produce and parse entire json documents, and when parsing provide a complete list of errors if there are any. Following that logic, you would only need a JsonConverter on the root types, and you could then put most of your validation and transformation logic in the converter. I am not sure to what degree this can be a good idea, and I would be tempted to prefer adding a JsonConverter to all of the types that I send/receive via json. If we can successfully disallow nulls in all the right places, then I think I'd be happy with that sort of solution.

I would welcome any discussion of these or other points.