thoth-org/Thoth.Json

Help required: Move field

davedawkins opened this issue · 7 comments

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

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 [||]
        })

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.

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:

  • if JSON "bar" is missing, this is Thing.v1 data, and so we are reading JSON "foo" into Thing.bar, and giving Thing.foo a default value
  • if JSON "bar" is present, use standard mappings (JSON "foo" -> Thing.foo, JSON "bar" -> Thing.bar)

@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!

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)

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
      ]

Beautiful thank you

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
        ]