Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vNext - Support lossless option #81

Closed
MangelMaxime opened this issue Sep 25, 2020 · 2 comments
Closed

vNext - Support lossless option #81

MangelMaxime opened this issue Sep 25, 2020 · 2 comments
Milestone

Comments

@MangelMaxime
Copy link
Contributor

Original issue: thoth-org/Thoth.Json.Giraffe#15

Hum, I guess the problem comes from the fact you are using 'T option option.

And we kind of erase the option type to a really simple representation:

* If `Some ...`, it outputs directly the value

* If `None`, it uses `null` or the absence of the value/property field.

So when nested several option we lose some information

open Fable.Core
open Thoth.Json

let someValue : string option= Some "Maxime"
let noneValue : string option = None
let someSomeValue : string option option = Some (Some "Maxime")
let someNoneValue : string option option = Some None
let deeplyNestedValue : string option option option option option = Some (Some (Some (Some None)))

JS.console.log(Encode.Auto.toString(4, someValue)) // "Maxime"
JS.console.log(Encode.Auto.toString(4, noneValue)) // null
JS.console.log(Encode.Auto.toString(4, someSomeValue)) // "Maxime"
JS.console.log(Encode.Auto.toString(4, someNoneValue)) // null
JS.console.log(Encode.Auto.toString(4, deeplyNestedValue)) // null

I am working on Thoth.Json 5 which is already doing some changes to how it represents some types perhaps we should make a custom representation for option type in order to retain the information. It will increase the JSON size but avoid losing information.

Right now, unless you copy/paste/adapt the code of the Auto modules you can't use it to solve your problem. However, you should be able to write your own Encode.option and Decode.option to have the desired behaviour I think.

Prototype:

There is a lot of code and I didn't focus on making it pretty just wanted to provide some hint for a potential solution. I think by using some helpers etc. it could look much better ^^

open Fable.Core
open Thoth.Json


// Standard behaviour from Thoth.Json Auto modules
module Standard =
    let someValue : string option= Some "Maxime"
    let noneValue : string option = None
    let someSomeValue : string option option = Some (Some "Maxime")
    let someNoneValue : string option option = Some None
    let deeplyNestedValue : string option option option option option = Some (Some (Some (Some None)))

    JS.console.log(Encode.Auto.toString(4, someValue)) // "Maxime"
    JS.console.log(Encode.Auto.toString(4, noneValue)) // null
    JS.console.log(Encode.Auto.toString(4, someSomeValue)) // "Maxime"
    JS.console.log(Encode.Auto.toString(4, someNoneValue)) // null
    JS.console.log(Encode.Auto.toString(4, deeplyNestedValue)) // null

module Custom =

    let log x = JS.console.log x

    module Encode =

        let losslessOption (encoder : 'a -> JsonValue) =
            fun value ->
                match value with
                | Some value ->
                    Encode.object 
                        [
                            "$type$", Encode.string "option"
                            "$state$", Encode.string "Some"
                            "$value$", encoder value
                        ]

                | None ->
                    Encode.object 
                        [
                            "$type$", Encode.string "option"
                            "$state$", Encode.string "None"
                        ]

    module Decode =
        
        let losslessOption (decoder : Decoder<'value>) : Decoder<'value option> =
            Decode.field "$type$" Decode.string
            |> Decode.andThen (fun typ ->
                match typ with
                | "option" ->
                    Decode.field "$state$" Decode.string
                    |> Decode.andThen (fun state ->
                        match state with
                        | "Some" ->
                            Decode.field "$value$" decoder |> Decode.map Some

                        | "None" ->
                            Decode.succeed None
                        
                        | invalid ->
                            "Expected an object with a field `$state$` set to `Some` or `None` but instead got `" + invalid + "`"
                            |> Decode.fail 
                    )

                | invalid ->
                    "Expected an object with a field `$type$` set to `option` but instead got `" + invalid + "`"
                    |> Decode.fail 
            )

    let someValue : string option= Some "Maxime"
    let noneValue : string option = None
    let someSomeValue : string option option = Some (Some "Maxime")
    let someNoneValue : string option option = Some None
    let deeplyNestedValue : string option option option option option = Some (Some (Some (Some None)))

    Encode.toString 4 (Encode.losslessOption Encode.string someValue)
    |> log

    Encode.toString 4 (Encode.losslessOption Encode.string noneValue)
    |> log

    Encode.toString 4 (Encode.losslessOption (Encode.losslessOption Encode.string) someSomeValue)
    |> log

    Encode.toString 4 (Encode.losslessOption (Encode.losslessOption Encode.string) someNoneValue)
    |> log

    Encode.toString 4 (Encode.losslessOption (Encode.losslessOption (Encode.losslessOption (Encode.losslessOption (Encode.losslessOption Encode.string)))) deeplyNestedValue)
    |> log

    match Decode.fromString (Decode.losslessOption Decode.string) (Encode.toString 4 (Encode.losslessOption Encode.string someValue)) with
    | Ok value ->
        match value with
        | Some value ->
            printfn "Got a Some ... %A" value
        
        | None ->
            printfn "Got a None"

    | Error err ->
        JS.console.error err

    match Decode.fromString (Decode.losslessOption Decode.string) (Encode.toString 4 (Encode.losslessOption Encode.string noneValue)) with
    | Ok value ->
        match value with
        | Some value ->
            printfn "Got a Some ... %A" value
        
        | None ->
            printfn "Got a None"

    | Error err ->
        JS.console.error err

    match Decode.fromString (Decode.losslessOption (Decode.losslessOption Decode.string)) (Encode.toString 4 (Encode.losslessOption (Encode.losslessOption Encode.string) someSomeValue)) with
    | Ok value ->
        match value with
        | Some (Some value) ->
            printfn "Got a Some (Some %A)" value
        
        | Some None ->
            printfn "Got a Some None"

        | None ->
            printfn "Got a None"

    | Error err ->
        JS.console.error err

REPL demo

@MangelMaxime MangelMaxime changed the title Support lossless option vNext - Support lossless option Nov 1, 2020
@MangelMaxime MangelMaxime added this to the Beyond milestone Jul 29, 2021
MangelMaxime added a commit that referenced this issue Jun 15, 2024
@joprice
Copy link

joprice commented Dec 19, 2024

A related issue I hit is being able to distinguish at least the top level presence of a field for things like a patch api where you want a ternary: present / absent / null, to allow distinguishing a field that should be unset versus one that is not being passed. I'm currently using a type Clearable that emulates this in my custom myriad generator.

@njlr
Copy link
Contributor

njlr commented Dec 19, 2024

A good search term is "skippable" e.g. https://github.com/Tarmil/FSharp.SystemTextJson/blob/master/docs/Format.md#skippable

MangelMaxime added a commit that referenced this issue Dec 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants