JSON Decode to Opaque Type

Hi all,

Pretty new to Elm and am having trouble figuring this out. I’ve been provided with (correct me if I am wrong) an opaque type and need to create a JSON Decoder for it.

Here’s a really stripped down example of what I’m working with:

type SomeType = SomeType
    { value1: String
    , value2: Int
    }

newSomeType = SomeType
    { value1 = ""
    , value2 = 0
    }

withValue1 : String -> SomeType -> SomeType
withValue1 value1 (SomeType someType) = SomeType { someType | value1 = value1}

withValue2 : Int -> SomeType -> SomeType
withValue2 value2 (SomeType someType) = SomeType { someType | value2 = value2}

It feels like there should be some elegant way to combine the builders with the individual field decoders into a nice pipeline… but I’ve been struggling with it.

someTypeDecoder : Decoder SomeType
someTypeDecoder = ???

result : Result Error SomeType
result = Decode.decodeString someTypeDecoder json

json : String
json = """
{
  "value1": "hello",
  "value2": 123
}
"""

Thanks for any help :slight_smile:

1 Like

Ok, so I’ve continued to bash my head at this and feel like I am getting closer (works but not as pretty as I’d like)…

decodeField : Decoder a -> (model -> a -> model) -> Decoder model -> Decoder model
decodeField fieldDecoder fieldSetter modelDecoder = Decode.map2 fieldSetter modelDecoder fieldDecoder

someTypeDecoder : Decoder SomeType
someTypeDecoder = Decode.succeed newSomeType
    |> decodeField (Decode.field "value1" Decode.string) (\model value1 -> withValue1 value1 model)
    |> decodeField (Decode.field "value2" Decode.int) (\model value2 -> withValue2 value2 model)

I think it can still be cleaned up. I’d like to get it to the point where I just pass the field decoder and setter

Well this has been a one sided conversation… but I did it :smiley:
Maybe this can act as a reference for someone. Also open to feedback if this looks completely wrong or bad practice.

someTypeDecoder : Decoder SomeType
someTypeDecoder = Decode.succeed newSomeType
    |> decode withValue1 (Decode.field "value1" Decode.string)
    |> decode withValue2 (Decode.field "value2" Decode.int)

decode : (a -> model -> model) -> Decoder a -> Decoder model -> Decoder model
decode fieldSetter fieldDecoder modelDecoder = Decode.map2 (flip fieldSetter) modelDecoder fieldDecoder

flip : (a -> b -> c) -> b -> a -> c
flip function b a = function a b
1 Like

Hi Joe!

Welcome! Your solution is great! It’s actually quite the library commonly used elm-json-decode-pipeline package. If I understand your problem correctly, then the native solution would be:

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  Decode.map2 SomeType
    (Decode.field "value1" Decode.string)
    (Decode.field "value2" Decode.int)

and using the common elm-json-decode-pipeline package:

import Json.Decode.Pipeline as Pipeline

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  Decode.succeed SomeType
    |> Pipeline.required "value1" Decode.string
    |> Pipeline.required "value2" Decode.int

Is this helpful? Do you have any other questions? The source for the central trick in elm-json-decode-pipeline is here!

EDIT: On second read, are you looking to add default values? (This ones in newSomeType?) In that case, you could do:

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  Decode.succeed SomeType
    |> Pipeline. optional "value1" Decode.string ""
    |> Pipeline. optional "value2" Decode.int 0

or use nullable or maybe in combination with Maybe.withDefault for a elm/json only approach!

The problem was that the type is opaque, so the simple solution you posted doesn’t work.
If SomeType were like this, it’d be fine:
type alias SomeType =
{ value1: String
, value2: Int
}
Unfortunately it is not and I cannot change it in this case.

1 Like

Ah, I see, sorry I totally missed that detail!

No worries, glad I figured this out. Elm is really a pleasure to learn.
The compiler has been a huge help in getting here.

1 Like

So glad to hear! :slight_smile:

1 Like

Hi Joe,

First of all, it’s not quite what I would call an Opaque type. An opaque type is one in which you cannot see the details. You achieve this in Elm via not exposing the constructors of the opaque type:

module SomeType exposing (SomeType) -- Note, not SomeType(..)

type SomeType =
    SomeType { value1 : String, value2 : Int }

Because of this, you would have to provide someway to create and use values within that module. The reason for doing this would be because there is some way to create an invalid value that you want to make impossible, so for example you might do something like this:

module Length exposing
    ( Length
    , fromInt
    , toInt
    )

type Length = Length Int

fromInt : Int -> Length
fromInt i =
    -- So it is impossible to create a negative length.
    Length (abs i)

toInt : Length -> Int
toInt l =
    case l of
         Length i ->
              i

Because it is impossible to create a value of type Length from outside of this module, we know that the only way to create a Length is by using Length.fromInt and therefore we know that all Lengths in this program will be non-negative. That’s what an opaque type is for me, but do note that there isn’t always universal agreement on typing terms.

The SomeType in your program, may or may not be oqaque from that definition, but even if is, you would need some way to create values of that type anyway. In addition, you would likely keep the definition of the decoder in the same module, so as far as that was concerned it wouldn’t be opaque. The SomeType in your program is what is generally called in Elm-land as a Custom type, or a Custom tagged type. What we mean is ‘one of those types that has a set of constructors in its type definition’.

Now, your actual question. As was suggested, if your SomeType was a type alias to a record type you could do the following:

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  Decode.map2 SomeType
    (Decode.field "value1" Decode.string)
    (Decode.field "value2" Decode.int)

You only need to tweak this a little bit in your case. The key point is that the first argument to Decode.map2 requires the type (a -> b -> c), which in this case is instantiated to String -> Int -> SomeType. You can give any function with that type, but the most obvious one would be:

someTypeDecoder : Decoder SomeType
someTypeDecoder =
  let
       createSomeType value1 value2 =
           SomeType { value1 = value1, value2 = value2 }
  in
  Decode.map2 createSomeType
    (Decode.field "value1" Decode.string)
    (Decode.field "value2" Decode.int)

You can do the analogous with the Pipeline version.

3 Likes