prozacchiwawa / elm-json-codec / JsonCodec

Build json decoders and encoders simultaneously, conserving the need for functions that do each in simple cases. This allows a single datum to embody both the encoder and the decoder for a type, reducing some duplication and redundancy. In simple cases, a codec can be built exclusively by composing other codecs.

Contributions by francescortiz

In more complex cases, first could be used, which allows fields to be encoded and extracted separately.

To just specify a decoder and encoder separately, use init.

module Session exposing (..)

import JsonCodec exposing (Codec)
import Dict exposing (Dict)
import Json.Decode as JD
import Json.Encode as JE

type alias Session =
    { queue : List String
    , playing : Maybe (String, Float)
    , likeCategories : Dict String Int
    }

type Alternative = A | Unknown String

-- An example showing the use of ```map``` to augment a
-- bijective encoding.
altcoder : Codec Alternative
altcoder =
    JsonCodec.string
    |> JsonCodec.map
        (\a -> if a == "A" then A else Unknown a)
        (\v ->
            case v of
                A -> "A"
                Unknown x -> x
        )

type Picky = OptA | OptB
pickyOpts : Dict String Picky
pickyOpts = Dict.fromList [("A", OptA), ("B", OptB)]

-- An example showing the use of ```andThen``` to exclude
-- bad values.
pickycoder : Codec Picky
pickycoder =
    JsonCodec.string |> JsonCodec.andThen
        (((flip Dict.get) pickyOpts)
        >> Maybe.map JD.succeed
        >> Maybe.withDefault (JD.fail "Option must be A or B")
        )
        (\p -> pickyOpts
        |> Dict.toList
        |> List.filterMap (\(k,v) -> if v == p then Just k else Nothing)
        |> List.head
        |> Maybe.withDefault "A"
        )

-- A codec that can stand on its own and also be reused.
playingSerializer : Codec (Maybe (String,Float))
playingSerializer =
    JsonCodec.nullable
        (JsonCodec.object2
             (\a b -> (a,b))
             ("u", JsonCodec.string, Tuple.first)
             ("p", JsonCodec.float, Tuple.second)
        )

-- Simple codec built with composition.
serializer : Codec Session
serializer =
    JsonCodec.object3
        Session
        ("queue", JsonCodec.list JsonCodec.string, .queue)
        ("playing", playingSerializer, .playing)
        ("like", JsonCodec.dict JsonCodec.int, .likeCategories)

-- Codec built in application style
type alias Test = 
    { i : Int, b : Bool, f : Float, o : Maybe String, s : String }

codec = 
    Test
    |> JC.first  "i" JC.int .i
    |> JC.next   "b" JC.bool .b
    |> JC.next   "f" JC.float .f
    |> JC.option "o" (JC.nullable JC.string) .o Nothing
    |> JC.next   "s" JC.string .s
    |> JC.end

x = JD.decodeString (JC.decoder be) 
     "{\"i\":3,\"b\":false,\"f\":3.14,\"s\":\"hi there\"}"
-- Ok { b = False, f = 3.14, i = 3, o = Nothing, s = "hi there" }
y = JD.decodeString (JC.decoder codec)
    "{\"i\":3,\"b\":false,\"f\":3.14,\"o\":\"hi\",\"s\":\"hi there\"}"
-- Ok { b = False, f = 3.14, i = 3, o = Just "hi", s = "hi there" }

Type


type Codec a

The type of codecs constructed by the library.

You can extract a Json.Decode.Decoder with decoder and a function that constructs Json.Encoder.Value with encoder.


type Builder a b

Type of a codec builder used with first, next and end

Simple codecs

string : Codec String

Codec matching and producing strings.

bool : Codec Basics.Bool

Codec matching and producing bools.

int : Codec Basics.Int

Codec matching and producing ints.

float : Codec Basics.Float

Codec matching and producing floats.

nullable : Codec a -> Codec (Maybe a)

Codec that maps null to Nothing and vice versa.

list : Codec a -> Codec (List a)

Codec that produces and consumes lists.

array : Codec a -> Codec (Array a)

Codec that produces and consumes arrays.

dict : Codec a -> Codec (Dict String a)

Codec that produces and consumes dictionaries of other values.

keyValuePairs : Codec a -> Codec (List ( String, a ))

Codec that produces and consumes key value pair lists of other values.

singleton : String -> Codec a -> Codec a

Codec that matches a single field similar to Json.Decode and produces a singleton object with 1 field.

object2 : (a -> b -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> Codec x

Codec that matches and produces objects with 2 given named fields.

object3 : (a -> b -> c -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> ( String, Codec c, x -> c ) -> Codec x

Codec that matches and produces objects with 3 given named fields.

object4 : (a -> b -> c -> d -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> ( String, Codec c, x -> c ) -> ( String, Codec d, x -> d ) -> Codec x

Codec that matches and produces objects with 4 given named fields.

object5 : (a -> b -> c -> d -> e -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> ( String, Codec c, x -> c ) -> ( String, Codec d, x -> d ) -> ( String, Codec e, x -> e ) -> Codec x

Codec that matches and produces objects with 5 given named fields.

object6 : (a -> b -> c -> d -> e -> f -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> ( String, Codec c, x -> c ) -> ( String, Codec d, x -> d ) -> ( String, Codec e, x -> e ) -> ( String, Codec f, x -> f ) -> Codec x

Codec that matches and produces objects with 6 given named fields.

object7 : (a -> b -> c -> d -> e -> f -> g -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> ( String, Codec c, x -> c ) -> ( String, Codec d, x -> d ) -> ( String, Codec e, x -> e ) -> ( String, Codec f, x -> f ) -> ( String, Codec g, x -> g ) -> Codec x

Codec that matches and produces objects with 7 given named fields.

object8 : (a -> b -> c -> d -> e -> f -> g -> h -> x) -> ( String, Codec a, x -> a ) -> ( String, Codec b, x -> b ) -> ( String, Codec c, x -> c ) -> ( String, Codec d, x -> d ) -> ( String, Codec e, x -> e ) -> ( String, Codec f, x -> f ) -> ( String, Codec g, x -> g ) -> ( String, Codec h, x -> h ) -> Codec x

Codec that matches and produces objects with 8 given named fields.

null : a -> Codec a

Codec that matches null, produces null.

succeed : a -> Json.Encode.Value -> Codec a

Codec that produces a constant decoded value and encodes to a constant value. One might use this to check a field with a constant value, such as a version number.

fail : String -> Json.Encode.Value -> Codec a

Codec that produces a constant encoded value but always fails decoding. One might use this while prefiltering inputs based on their structure but ensure that encoded json has the right structure.

Transform in both directions

map : (a -> b) -> (b -> a) -> Codec a -> Codec b

Wrap the codec in a transformer that produces and consumes another type.

type Alternative = A | Unknown String

altcoder : Codec Alternative
altcoder =
    Codec.string
    |> Codec.map
        (\a -> if a == "A" then A else Unknown a)
        (\v ->
            case v of
                A -> "A"
                Unknown x -> x
        )

andThen : (a -> Json.Decode.Decoder b) -> (b -> a) -> Codec a -> Codec b

Like map, but the decode function returns a decoder that will be evaluated next, rather than just mapping the value.

-- An example showing the use of ```andThen``` to exclude
-- bad values.
pickycoder : Codec Picky
pickycoder =
    JsonCodec.string |> JsonCodec.andThen
        (((flip Dict.get) pickyOpts)
        >> Maybe.map JD.succeed
        >> Maybe.withDefault (JD.fail "Option must be A or B")
        )
        (\p -> pickyOpts
        |> Dict.toList
        |> List.filterMap (\(k,v) -> if v == p then Just k else Nothing)
        |> List.head
        |> Maybe.withDefault "A"
        )

Decoding alternatives

oneOf : List (Json.Decode.Decoder a) -> (a -> Json.Encode.Value) -> Codec a

Match one of many decoders, encode using the given function.

Construction

decoder : Codec a -> Json.Decode.Decoder a

Get a Json.Decode.Decoder from a codec.

encoder : Codec a -> a -> Json.Encode.Value

Get a function that encodes Json.Encode.Value from a codec.

init : Json.Decode.Decoder a -> (a -> Json.Encode.Value) -> Codec a

Construct an arbitrary codec from a decoder and an encoder function.

Application

start : b -> Builder (Json.Decode.Decoder b) (o -> List ( String, Json.Encode.Value ))

Start composing a codec.

first : String -> Codec v -> (o -> v) -> (v -> b) -> Builder (Json.Decode.Decoder b) (o -> List ( String, Json.Encode.Value ))

Start composing a codec to decode a record using a series of function applications.

Using:

Return:

You can build record codecs of arbitrarily many parameters with this, the same way other codecs are built, together using the same code.

An example:

type alias X = { i : Int, s : String, b : Bool, f : Float }
c =  JC.first "i" JC.int .i X 
  |> JC.next "s" JC.string .s
  |> JC.next "b" JC.bool .b
  |> JC.next "f" JC.float .f
  |> JC.end

> JD.decodeString (JC.decoder c) "{\"i\":3,\"s\":\"hi\",\"b\":false,\"f\":1.9}"
Ok { i = 3, s = "hi", b = False, f = 1.9 } : Result.Result String Repl.X

firstOpt : String -> Codec v -> (o -> v) -> v -> (v -> b) -> Builder (Json.Decode.Decoder b) (o -> List ( String, Json.Encode.Value ))

Begin decoding with an optional field. As first but a default value is added.

next : String -> Codec v -> (o -> v) -> Builder (Json.Decode.Decoder (v -> b)) (o -> List ( String, Json.Encode.Value )) -> Builder (Json.Decode.Decoder b) (o -> List ( String, Json.Encode.Value ))

Continue a partial codec from first, satisfying one more parameter of the constructor function.

option : String -> Codec v -> (o -> v) -> v -> Builder (Json.Decode.Decoder (v -> b)) (o -> List ( String, Json.Encode.Value )) -> Builder (Json.Decode.Decoder b) (o -> List ( String, Json.Encode.Value ))

Allow pipelines to decode optional fields, not just optional values.

end : Builder (Json.Decode.Decoder o) (o -> List ( String, Json.Encode.Value )) -> Codec o

Make the final step to turn a result from Builder into Codec.