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

Parsing a discriminated union #177

Closed RicoSaupeBosch closed 1 year ago

RicoSaupeBosch commented 1 year ago

Hi. How would i parse this json?

 "state": {
            "case": "BlankMigrationState",
            "fields": [
                {
                    "case": "New"
                }
            ]
        }

It is generated from this DU

 type RequestState =
        | BlankMigrationState of BlankState
        | IlmMigrationState of IlmState

where Blankstate is a DU of multiple values

[<RequireQualifiedAccess>]
    type BlankState =
        | New
        | ImpedimentCheck
...

I though of starting with that.

fields.Required.At["state"] requestStateDecoder

image

But that fails with

image

What am I doing wrong here?

Thanks, Rico

MangelMaxime commented 1 year ago

Hello,

Required.At is for decoding arrays if you look at the error message it is explaining you that it was expecting a list but found an object.

If you want to decode an object, you want to use Required.Field.

When writing code in marking, you can use ``` around your code block to make the format easier to read.

This a code block with no language associated to it


```fs
// Here you are inside an F# code block
MangelMaxime commented 1 year ago

Here is working snippet for the JSON you provided:

open Fable.Core
open Thoth.Json

let json =
    """
{
  "state": {
    "case": "BlankMigrationState",
    "fields": [
      {
        "case": "New"
      }
    ]
  }
}
    """

type BlankState =
    | New
    | ImpedimentCheck

module BlankState =

    let decoder : Decoder<BlankState> =
        Decode.field "case" Decode.string
        |> Decode.andThen (fun caseValue ->
            match caseValue with
            | "New" -> Decode.succeed New
            | invalidCase -> 
                sprintf "'%s' is invalid case for BlankState" invalidCase
                |> Decode.fail
        )

type RequestState =
    | BlankMigrationState of BlankState

module RequestState =

    let decoder : Decoder<RequestState> =
        Decode.field "case" Decode.string
        |> Decode.andThen (fun caseValue ->
            match caseValue with
            | "BlankMigrationState" -> 
                Decode.field "fields" (Decode.index 0 BlankState.decoder)
                |> Decode.map BlankMigrationState
            | invalidCase -> 
                sprintf "'%s' is invalid case for RequestState" invalidCase
                |> Decode.fail
        )

let result = 
    Decode.unsafeFromString 
        (Decode.field "state" RequestState.decoder) 
        json

JS.console.log result

Fable REPL

RicoSaupeBosch commented 1 year ago

Thanks for the example. If I use your code with Thoth.Json.Net then it works fine. I am using Thoth.Json and the json from the api looks like that.

{        "modifiedBy": "name",
        "state": {
            "case": "BlankMigrationState",
            "fields": [
                {
                    "case": "New"
                }
            ]
        }

fs code is

    let blankStateDecoder : Decoder<BlankState> =
        Decode.field "case" Decode.string
        |> Decode.andThen (fun caseValue ->
            let value = fromStringI<BlankState> caseValue
            match value with
            | Some v -> Decode.succeed v
            | None -> $"{caseValue} is invalid case for BlankState" |> Decode.fail)

    let requestStateDecoder : Decoder<RequestState> =
        Decode.field "case" Decode.string
        |> Decode.andThen (fun caseValue ->
            match caseValue with
            | "BlankMigrationState" -> 
                Decode.field "fields" (Decode.index 0 blankStateDecoder)
                |> Decode.map RequestState.BlankMigrationState
            | invalidCase -> $"{invalidCase} is invalid case for RequestState" |> Decode.fail)

type BlankState =
    | New
    | ImpedimentCheck

type RequestState =
    | BlankMigrationState of BlankState

type Request =
        { ModifiedBy: string
          mutable State: RequestState }

let RequestDecoder: Decoder<Request> =
        Decode.object(fun fields -> {
            ModifiedBy= fields.Required.Field "modifiedBy" Decode.string
            State = fields.Required.Field "state" requestStateDecoder 
        })

Still I get the same error. Did I get your solution wrong?

image

MangelMaxime commented 1 year ago

@RicoSaupeBosch Without the full code I can't help you.

For example, the fromStringI function is something custom and I don't know the definition of it.

The error is explaining you what it is expecting and what it got so you need to check if you didn't make a type in the decoder definition or pass the wrong JSON/value tc.

RicoSaupeBosch commented 1 year ago

Yes. that is correct. This function gets a DU option from a string

image

MangelMaxime commented 1 year ago

Please use code block when sharing code, as it allow copy/paste.

I tried to run the code you provided me with, and I don't have the same error as you do:

Code run:

module Main

open Feliz
open Browser.Dom

open Fable.Core
open Thoth.Json
open FSharp.Reflection
open System.Text
open System

let json =
    """
{        "modifiedBy": "name",
        "state": {
            "case": "BlankMigrationState",
            "fields": [
                {
                    "case": "New"
                }
            ]
        }
        }
    """

type BlankState =
    | New
    | ImpedimentCheck

type RequestState =
    | BlankMigrationState of BlankState

let inline fromStringI<'a> (s : string) =
    let unionCases =
        FSharpType.GetUnionCases typeof<'a>
        |> Array.filter (fun case ->
            StringComparer.CurrentCultureIgnoreCase.Equals(case.Name, s)
        )

    match unionCases with
    | [| case |] -> Some(FSharpValue.MakeUnion(case, [||]) :?> 'a)
    | _ -> None

let blankStateDecoder : Decoder<BlankState> =
    Decode.field "case" Decode.string
    |> Decode.andThen (fun caseValue ->
        let value = fromStringI<BlankState> caseValue
        match value with
        | Some v -> Decode.succeed v
        | None -> sprintf "%s is invalid case for BlankState" caseValue |> Decode.fail)

let requestStateDecoder : Decoder<RequestState> =
    Decode.field "case" Decode.string
    |> Decode.andThen (fun caseValue ->
        match caseValue with
        | "BlankMigrationState" ->
            Decode.field "fields" (Decode.index 0 blankStateDecoder)
            |> Decode.map RequestState.BlankMigrationState
        | invalidCase -> sprintf "%s is invalid case for RequestState" caseValue |> Decode.fail)

type Request =
        { ModifiedBy: string
          mutable State: RequestState }

let RequestDecoder: Decoder<Request> =
        Decode.object(fun fields -> {
            ModifiedBy= fields.Required.Field "modifiedBy" Decode.string
            State = fields.Required.Field "state" requestStateDecoder
        })

let result =
    Decode.unsafeFromString RequestDecoder json

JS.console.log result

Result:

Error at: `$.state.fields.[0]`
The following `failure` occurred with the decoder: New is invalid case for BlankState
    at unsafeFromString (Decode.fs.js:128:15)
    at Main.fs.js?t=1683297937460:91:23
u

Also if you look at the Fable output you will see that there is compilation warning/error because StringComparer is not supported by Fable

./Main.fs(38,12): (38,51) error FABLE: System.StringComparer.get_CurrentCultureIgnoreCase (static) is not supported by Fable - Inline call from .(48,20)
./Main.fs(38,12): (38,72) error FABLE: System.StringComparer.Equals is not supported by Fable - Inline call from .(48,20)
Fable compilation finished in 13ms

./Main.fs(1,1): warning FABLE: Fable only supports a subset of standard .NET API, please check https://fable.io/docs/dotnet/compatibility.html. For external libraries, check whether they are Fable-compatible in the package docs.

Changing the comparaison to case.Name.ToLowerInvariant() = s.ToLowerInvariant() make the code works on my side:

Full working snippet that I used:

module Main

open Feliz
open Browser.Dom

open Fable.Core
open Thoth.Json
open FSharp.Reflection
open System.Text
open System

let json =
    """
{        "modifiedBy": "name",
        "state": {
            "case": "BlankMigrationState",
            "fields": [
                {
                    "case": "New"
                }
            ]
        }
        }
    """

type BlankState =
    | New
    | ImpedimentCheck

type RequestState =
    | BlankMigrationState of BlankState

let inline fromStringI<'a> (s : string) =
    let unionCases =
        FSharpType.GetUnionCases typeof<'a>
        |> Array.filter (fun case ->
            case.Name.ToLowerInvariant() = s.ToLowerInvariant()
        )

    match unionCases with
    | [| case |] -> Some(FSharpValue.MakeUnion(case, [||]) :?> 'a)
    | _ -> None

let blankStateDecoder : Decoder<BlankState> =
    Decode.field "case" Decode.string
    |> Decode.andThen (fun caseValue ->
        let value = fromStringI<BlankState> caseValue
        match value with
        | Some v -> Decode.succeed v
        | None -> sprintf "%s is invalid case for BlankState" caseValue |> Decode.fail)

let requestStateDecoder : Decoder<RequestState> =
    Decode.field "case" Decode.string
    |> Decode.andThen (fun caseValue ->
        match caseValue with
        | "BlankMigrationState" ->
            Decode.field "fields" (Decode.index 0 blankStateDecoder)
            |> Decode.map RequestState.BlankMigrationState
        | invalidCase -> sprintf "%s is invalid case for RequestState" caseValue |> Decode.fail)

type Request =
        { ModifiedBy: string
          mutable State: RequestState }

let RequestDecoder: Decoder<Request> =
        Decode.object(fun fields -> {
            ModifiedBy= fields.Required.Field "modifiedBy" Decode.string
            State = fields.Required.Field "state" requestStateDecoder
        })

let result =
    Decode.unsafeFromString RequestDecoder json

JS.console.log result
RicoSaupeBosch commented 1 year ago

Thanks for all you support. It was indeed that warning that caused my problem. :( Its working now perfectly fine. Thanks again.