Codec with non-matching structures: object/custom type

I have a type that I currently have encoders/decoders for. I want to turn them into elm-codecs but I’m really struggling.

Here’s the Elm type:

type D
    = A { f : Int }
    | B { b : String }
    | C { f : Int }

And here’s the JSON structure :

{"type": "A", "f": 123}
or
{"type": "B", "b": "blah"}
or
{"type": "C", "f": 456}

Here’s how it works currently (on the decoding side):

  • a Decode.oneOf tries aDecoder, bDecoder and cDecoder
  • each of those checks the fields it needs, and then (Decode.andThen) asserts that the type field has the right value.

This bit with the “type” being a field of the data object in the JSON, while being a variant in the Elm type, I can’t seem to find a way to make it translate to elm-codec. Is that just normal, that the codec technique requires you to have similar structures on the Elm and JSON sides? Or is there a way? (I tried something with Codec.andThen but got nowhere.)

https://package.elm-lang.org/packages/miniBill/elm-codec/latest/Codec#custom

I think the choice of field that is used to determine the variant (“type” in your case) is hard coded into elm-codec? Looking at the code I think it might be “tag” but I didn’t manage to fully grok it yet.

Yes it gives something like {"tag":"…","args":[…]}.
I was wondering if there was a more advanced way to get around it, maybe redesigning my type differently. (But that would mean quite a bit of refactoring now, and I might not have enough time budget on this project!)

If you already have a decoder and encoder you can build a Codec out of them by using:

https://package.elm-lang.org/packages/miniBill/elm-codec/latest/Codec#build

Interesting! But what is the point of using a Codec if I’m building it out of two potentially out-of-sync (en|de)coders?

You might just be building part of a larger Codec, where most of it is standard elm-codec, but you need just a few parts completely custom. If elm-codec gives you nothing, then don’t use it and stick with standard encoder+decoder.

Right, I understand. I thought about sticking to elm-codec just for the record part (the variants’ argument). That’s what I started with, the only problem I have is that I don’t know how to/if I can reproduce this constraint on the value of the type field.

I can write this:

Codec.object
  (\type f -> { f = f }) 
  |> Codec.field "type" (always "A") Codec.string
  |> Codec.field "f" .f Codec.int
  |> Codec.buildObject  

Encoding works fine, and puts the type in there, but decoding ignores the type field, so it works even in type is different (which defeats the purpose of this field, which is to disambiguate between different types of data which might use the same fields).

Here is an Ellie that I just put together that shows it all:

  • encoding is successful
  • decoding from the previously encoded string is successful, but…
  • decoding is also successful if the type field is nonsense

https://ellie-app.com/mHSVMp22sj7a1 (fixed, again!)

Woops that Ellie link was nonsense, here’s the right one: https://ellie-app.com/mHSVMp22sj7a1

miniBill/elm-codec is very opinionated when it comes to what the JSON looks like. We ran into problems with that, just like you do, when trying to introduce it at work. Partly because we had to support already existing JSON structures, partly because we read the actual JSON a lot in Postgres and having to do 'args'->0 all the time there was annoying.

So we forked elm-codec into our own version where you can customize the “tag” field name, and put data in whatever fields you like (instead of "args") and a bunch of other little tweaks and changes (like better error messages). I can’t share that code though.

Ok I guess I’m in that sort of case then. It’s not worth the time for me to find a better solution for this specific problem, so I guess that’s it. Thank you both for your help!

Something like that might make a nice PR against elm-codec.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.