thoth-org / Thoth.Json

Library for working with JSON in a type safe manner, this libs is targeting Fable
https://thoth-org.github.io/Thoth.Json/
MIT License
150 stars 36 forks source link

Add ability to "fail" inside the object decoder #121

Open MangelMaxime opened 3 years ago

MangelMaxime commented 3 years ago

Currently, when inside the object decoder user cannot fail in it.

For example in this code, the user needs to "escape" the object decoder space in order to be able to use Decode.fail

REPL demo

type Bar =
    {
        Name : string
    }

module Bar =

    let decoder : Decoder<Bar> =
        Decode.object (fun get -> 
            {
                Name = get.Required.Field "name" Decode.string
            }
        )

type Vee =
    {
        Blah : string
    }

module Vee =

    let decoder : Decoder<Vee> =
        Decode.object (fun get -> 
            {
                Blah = get.Required.Field "blah" Decode.string
            }

        )

type Wrapper =
    {
        Bar : Bar option
        Vee : Vee option
    }

module Wrapper =

    let decoder : Decoder<Wrapper> =

        Decode.field "type" Decode.string
        |> Decode.andThen (
            function
            | "foo" ->
                Decode.object (fun get ->
                    let barOpt = get.Optional.Field "bar" Bar.decoder
                    let veeOpt = get.Optional.Field "vee" Vee.decoder

                    barOpt, veeOpt
                )
                |> Decode.andThen (fun (barOpt, veeOpt) ->

                    match barOpt, veeOpt with
                    | Some bar, None ->
                        {
                            Bar = Some bar
                            Vee = None
                        }
                        |> Decode.succeed

                    | None, Some vee ->
                        {
                            Bar = None
                            Vee = Some vee
                        }
                        |> Decode.succeed

                    | Some _, Some _ ->
                        Decode.fail "Both 'bar' and 'vee' cannot be set at the same time"

                    | None , None ->
                        Decode.fail "At least one of 'bar' and 'vee' should be set"

                )

            | invalid ->
                sprintf "'%s' is an invalid type for Wrapper" invalid
                |> Decode.fail
        )

The problem solved by the code above is:

image

MMagueta commented 2 years ago

Let me see if I understood, we want to map JSON structures that can variate, allowing a decoder to fail and opt for another choice, is that correct?

If that's the case I did something on that line at work a few weeks ago. I had an algebraic data type that would return a structure for case A and another for case B, then I had to check it and let it fail on the decoding. The way I solved it was with a computation expression. Here is the snippet:

static member Decoder(okDecoder: Decoder<'A>) : Decoder<APIMessage<'A>> =
    let decode = DecodeBuilder()

    Decode.oneOf [
        decode {
            let! error = Decode.object (fun get -> get.Required.Raw ApplicationError.Decoder)
            return Failure error
        } |> ``Decoder.Run``
        decode {
            let! response = Decode.object (fun get -> get.Required.Raw okDecoder)
            return Response response
        } |> ``Decoder.Run``
    ]

So, explaining the code, I opted to receive a generic decoder for the OK case (which can variate into many shades), while the other option is always ApplicationError (hence the lack of a errorDecoder). The computation expression does the job of unpacking the Decode.object for me, so I just see which one is valid.

If that seems ok I can replicate it by adding a similar computation expression to the library. That could even be an opportunity to mimic a little the behavior of FParsec adding some infix operators. What do you think @MangelMaxime?

MangelMaxime commented 2 years ago

Hum, here the problem was that we needed to validate that only a specific set of the fields was set.

From the example, you provided I am unsure of what the decoders are doing. If you want to try explain it, I found that in general having the Domain Types + Decoders + different JSON with the expected results helps.

About adding, new syntax to Thoth.Json. In general, I am not opposed to it, I decide on case by case. For example, this is how the object decoder style has been added.

One good thing about Thoth.Json is that people can enhance it via a library and/or in their own project directly if needed.