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
152 stars 36 forks source link

Help required: Move field #186

Closed davedawkins closed 8 months ago

davedawkins commented 8 months ago

I have this JSON on disk

{
       "foo": [ "cat", "dog" ]
}

which maps to type

type Thing = { foo : string[] }

I have changed Thing to be

type Thing = { foo : string[]; bar : string [] }

and now the foo on disk must go into Thing.bar (and Thing.foo will be empty)

I know how to handle bar being missing, by using custom decoders, but cannot figure out how to write a Thing decoder which can read foo into Thing.bar if bar is missing in the JSON.

Thank you

njlr commented 8 months ago

Perhaps something like this?

#r "nuget: Thoth.Json.Net, 11.0"

type Thing = { foo : string[]; bar : string [] }

module Thing =

  open Thoth.Json.Net

  let decode : Decoder<Thing> =
    Decode.object
      (fun get ->
        {
          foo = get.Required.Field "foo" (Decode.array Decode.string)
          bar =
            get.Optional.Field "bar" (Decode.array Decode.string)
            |> Option.defaultValue [||]
        })
MangelMaxime commented 8 months ago

Hello @davedawkins,

I am not sure to understand everything. Could you please provide the expected values associated to the JSON?

For example:

type Thing = { foo : string[]; bar : string [] }

let json =
    """
{
       "foo": [ "cat", "dog" ]
}
    """

let expected =
    {
        foo = [ "cat", "dog" ]
        bar = []
    }

If there are different scenario I would be interested in having them too.

davedawkins commented 8 months ago

Hello @davedawkins,

I am not sure to understand everything. Could you please provide the expected values associated to the JSON? ... If there are different scenario I would be interested in having them too.

type Thing = { foo : string[]; bar : string [] }   // bar is a new field for Thing v2

let json1 = // Legacy Thing v1 data
    """
{
       "foo": [ "cat", "dog" ]    // Must now target Thing.bar
}
    """

let json2 = // Example Thing v2 data
    """
{
       "foo": [ "apple", "peach" ]
       "bar": [ "cat", "dog" ]
}
    """

let expectedFromJson1 =
    {
        foo = []
        bar = [ "cat", "dog" ]
    }
let expectedFromJson2 =
    {
        foo = [ "apple", "peach" ]
        bar = [ "cat", "dog" ]
    }

The rule is:

@njlr Thank you, but it misses the JSON "foo" to Thing.bar mapping

Please don't judge me for creating this upgrade scenario....

Thank you!

davedawkins commented 8 months ago

My best effort

  type Thing_v1 = { foo : string[] }
  let decoderThing : Decoder<Thing> =
      fun path value ->
          let r = 
              if (JsHelpers.jsPropertyExists("bar",value)) then
                  Decode.Auto.fromString<Thing>( Encode.Auto.toString(value))
              else
                  match Decode.Auto.fromString<Thing_v1>( Encode.Auto.toString(value)) with
                  | Ok tv1 -> Ok ({ foo = [||]; bar = tv1.foo })
                  | Error _ as e -> e

          match r with
          | Ok v -> Ok v
          | Error msg -> Error <| DecoderError (msg, ErrorReason.FailMessage msg)
njlr commented 8 months ago

I think you want Decode.oneOf

  let decoderThing : Decoder<Thing> = 
    Decode.oneOf 
      [
         decoderV2
         decoderV1
      ]

Beware that the order matters here! It will attempt decoderV2 first and only try decoderV1 if that fails.

You can also use Decode.map if you need to do a transformation:

  let decoderThing : Decoder<Thing> = 
    Decode.oneOf 
      [
        decoderV2
        decoderV1 |> Decode.map convertV1ToV2
      ]
davedawkins commented 8 months ago

Beautiful thank you

davedawkins commented 8 months ago

Can confirm this works, thank you so much

    let decoder : Decoder<EntityDTO> =
        Decode.oneOf [
            Decode.Auto.generateDecoder<EntityDTO>()
            Decode.Auto.generateDecoder<EntityDTO_v1>() |> Decode.map mapE1E2
        ]