Search code examples
jsondecodeelmdecoder

How to write a Json decoder that converts from type ModelV1 to type ModelV2


I am finding it very hard to migrate old Model state to a new Model state.

Let's say initially our code had Model, and we persisted it to localstorage.

Now we want to add one additional field, "createdAt" to our Model, so I created a new type alias for the same.

import Json.Decode as D
import Html as H


type alias Todo = {id:Int, title: String}

jsonV1 = """

  {"id":1, "title":"Elm-Rocks"}

"""

jsonV2 = """

  {"id":1, "title":"Elm-Rocks", "createdAt":1479633701604}


"""


type alias TodoV2 = {id:Int, title: String, createdAt:Int}

decode = D.map2 Todo
  (D.field "id" D.int)
  (D.field "title" D.string)

decodeV2 = D.map3 TodoV2
  (D.field "id" D.int)
  (D.field "title" D.string)
  (D.field "createdAt" D.int)


result1 = D.decodeString decode jsonV1

result2 = D.decodeString decodeV2 jsonV2


type alias Model = {todos: List Todo}
type alias ModelV2 = {todos: List TodoV2}


main = H.div[] [ 
    H.div[][H.text (toString result1)]
  , H.div[][H.text (toString result2)]
  ]

How to write a decoder/function, that accepts any of v1/v2 format json string and give me a ModelV2 record.

I am aware of Decoder.andThen but I don't know how to write the implementation for todoDecoderV1: ??? -> TodoV2


Solution

  • You can use Json.Decode.oneOf to try parsers and provide a default fallback by using Json.Decode.succeed. If you wanted to represent the absence of createdAt as a 0, you could write your decoder like this:

    decode = D.map3 TodoV2
      (D.field "id" D.int)
      (D.field "title" D.string)
      (D.oneOf [(D.field "createdAt" D.int), D.succeed 0])
    

    However, to more accurately represent reality, I would recommend that your Model be changed so that createdAt is optional by changing its type to a Maybe Int. This is a great way to make impossible states impossible.

    type alias TodoV3 = {id:Int, title: String, createdAt:Maybe Int}
    
    decodeV3 = D.map3 TodoV3
      (D.field "id" D.int)
      (D.field "title" D.string)
      (D.maybe (D.field "createdAt" D.int))