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

Documenting different ways to encode DUs #103

Open
Choc13 opened this issue Feb 27, 2021 · 6 comments
Open

Documenting different ways to encode DUs #103

Choc13 opened this issue Feb 27, 2021 · 6 comments

Comments

@Choc13
Copy link

Choc13 commented Feb 27, 2021

It seems there are (at least) a couple of ways that people encode DUs in JSON.

The first way, which is currently documented, uses the case name as the property name which then encloses that case's data. For example the following F# type

type Foo = { X: int }
type Bar = { Y: string }

type Baz =
    | Foo of Foo
    | Bar of Bar

The Foo case would be encoded as

{
    "foo": {
        "x": 1
    }
}

And the Bar case would be encoded as:

{
    "bar": {
        "y": "string"
    }
}

I think this way makes a lot of sense when you want to round trip to a DU on both sides.

A second way, which isn't documented, that is common if you're using TypeScript on the client is to enclose the case label under some "tag" prop. This is even more relevant I think if you have some DU cases that don't have any data. For example:

type Foo = { X: int }

type Baz =
    | Foo of Foo
    | Bar 

Then the Foo case would be encoded as:

{
    "type": "foo",
    "x": 1
}

And the Bar case would be:

{
    "type": "bar"
}

Where obviously the "type" field could be named anything.

I think this second case needs documenting, along with any other ways of commonly encoding DUs, as it's not immediately obvious how to build a codec for this second type.

I think the way you want to go about writing the codec for this second way is to start with codecs for the data in each of the case labels and then apply a combinator that adds a tag prop for the relevant case and (maybe a second combinator) that lifts the codec into the DU type by applying the case constructor when decoding and unwrapping the value when encoding. You then want to use an alternative to bring all of those codecs together.

I'm happy to open a PR to improve the docs to add an example for the second case.

@gusty
Copy link
Member

gusty commented Mar 3, 2021

Is there any other relevant case for encoding DUs ?

If you feel like starting a PR go ahead, please try to use something more concrete than foo, bar, at least that's my personal preference when reading docs.

@Choc13
Copy link
Author

Choc13 commented Mar 3, 2021

I've had a look through some aeson examples / tutorials and it seems like the two I've listed above are indeed the common DU encodings. I'll start working on a PR when I get some free time later to document this, and yeah I'll try to use a more concrete example to highlight it.

@Choc13
Copy link
Author

Choc13 commented Mar 4, 2021

In order to write a codec for a "tagged DU" (the second case above), I've had to define this jfromWith function (similar to the one you pointed me to on SO), which looks like:

let inline jfromWith codec (getter: 'T -> 'Value option) =
        let (decoder, encoder) = codec |> Codec.ofConcrete

        (decoder,
         fun x ->
             match x |> getter with
             | Some (v: 'Value) -> v |> encoder
             | None -> [] |> readOnlyDict)
        |> Codec.toConcrete

So that the resulting DU codec would look like:

type Baz =
    | Foo of Foo
    | Bar 
    static member JsonObjCodec =
        jchoice [ 
           (fun _ (x: Foo) -> x |> Foo)
               <!> jreq "type" (fun (_: Baz) -> "foo" |> Some)
               <*> jfromWith Foo.JsonObjCodec (function | Foo f -> f |> Some | _ -> None)
           (fun _ -> Bar)
               <!> jreq "type" (fun (_: Baz) -> "bar" |> Some)
       ]

It feels like it would be worth adding jfromWith to the library first so we can then reference it in the documentation. Thoughts?

@Choc13
Copy link
Author

Choc13 commented Mar 4, 2021

It looks like #100 covers quite a bit of ground already. I think the one thing that is missing from there is the fact that you might want to build the DU codec from existing codecs for the case data. But it seems like it would make sense for that PR to be merged first as it's definitely a step in the right direction.

@gusty
Copy link
Member

gusty commented Mar 4, 2021

Sure, but we can still draft this part of the docs and once that PR is merged, create a PR to integrate it there.

Regarding jfromWith I think we can add it, but isn't it better to publish a snippet, get some usage feedback and eventually add it, or add something better? I mean, I could have added the original version and now realized that it doesn't cover for your case.

@Choc13
Copy link
Author

Choc13 commented Mar 5, 2021

OK that's cool. I'll work from that branch in the other PR then I think as I can build from the examples there.

As for jfromWith I'll leave that up to your discretion. I you think it's better that we just include the snippet and battle test it a bit more first then that's fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants