MartinSStewart / elm-serialize / Serialize

Serialization

You have three options when encoding data. You can represent the data either as json, bytes, or a string. Here's some advice when choosing:

*encodeToJson is more compact when encoding integers with 6 or fewer digits. You may want to try both encodeToBytes and encodeToJson and see which is better for your use case.

encodeToJson : Codec e a -> a -> Json.Encode.Value

Convert an Elm value into json data.

decodeFromJson : Codec e a -> Json.Encode.Value -> Result (Error e) a

Run a Codec to turn a json value encoded with encodeToJson into an Elm value.

encodeToBytes : Codec e a -> a -> Bytes

Convert an Elm value into a sequence of bytes.

decodeFromBytes : Codec e a -> Bytes -> Result (Error e) a

Run a Codec to turn a sequence of bytes into an Elm value.

encodeToString : Codec e a -> a -> String

Convert an Elm value into a string. This string contains only url safe characters, so you can do the following:

import Serialize as S

myUrl =
    "www.mywebsite.com/?data=" ++ S.encodeToString S.float 1234

and not risk generating an invalid url.

decodeFromString : Codec e a -> String -> Result (Error e) a

Run a Codec to turn a String encoded with encodeToString into an Elm value.

getJsonDecoder : (e -> String) -> Codec e a -> Json.Decode.Decoder a

Get the decoder from a Codec which you can use inside a elm/json decoder.

import Json.Decode
import Serialize

type alias Point =
    { x : Float, y : Float }

pointCodec : Serialize.Codec e Point
pointCodec =
    Serialize.record Point
        |> Serialize.field .x Serialize.float
        |> Serialize.field .y Serialize.float
        |> Serialize.finishRecord

pointDecoder : Json.Decode.Decoder Point
pointDecoder =
    -- Since pointCodec doesn't have any custom error values, we can use `never` for our errorToString parameter.
    Serialize.getJsonDecoder never pointCodec

Definition


type Codec e a

A value that knows how to encode and decode an Elm data structure.


type Error e
    = CustomError e
    | DataCorrupted
    | SerializerOutOfDate

Possible errors that can occur when decoding.

*It's possible for corrupted data to still succeed in decoding (but with nonsense Elm values). This is because internally we're just encoding Elm values and not storing any kind of structural information. So if you encoded an Int and then a Float, and then tried decoding it as a Float and then an Int, there's no way for the decoder to know it read the data in the wrong order.

Primitives

string : Codec e String

Codec for serializing a String

bool : Codec e Basics.Bool

Codec for serializing a Bool

float : Codec e Basics.Float

Codec for serializing a Float

int : Codec e Basics.Int

Codec for serializing an Int

unit : Codec e ()

Codec for serializing () (aka Unit).

bytes : Codec e Bytes

Codec for serializing Bytes. This is useful in combination with mapValid for encoding and decoding data using some specialized format.

import Image exposing (Image)
import Serialize as S

imageCodec : S.Codec String Image
imageCodec =
    S.bytes
        |> S.mapValid
            (Image.decode >> Result.fromMaybe "Failed to decode PNG image.")
            Image.toPng

byte : Codec e Basics.Int

Codec for serializing an integer ranging from 0 to 255. This is useful if you have a small integer you want to serialize and not use up a lot of space.

import Serialize as S

type alias Color =
    { red : Int
    , green : Int
    , blue : Int
    }

color : S.Codec e Color
color =
    Color.record Color
        |> S.field .red byte
        |> S.field .green byte
        |> S.field .blue byte
        |> S.finishRecord

Warning: values greater than 255 or less than 0 will wrap around. So if you encode -1 you'll get back 255 and if you encode 257 you'll get back 1.

Data Structures

maybe : Codec e a -> Codec e (Maybe a)

Codec for serializing a Maybe

import Serialize as S

maybeIntCodec : S.Codec e (Maybe Int)
maybeIntCodec =
    S.maybe S.int

list : Codec e a -> Codec e (List a)

Codec for serializing a List

import Serialize as S

listOfStringsCodec : S.Codec e (List String)
listOfStringsCodec =
    S.list S.string

array : Codec e a -> Codec e (Array a)

Codec for serializing an Array

dict : Codec e comparable -> Codec e a -> Codec e (Dict comparable a)

Codec for serializing a Dict

import Serialize as S

type alias Name =
    String

peoplesAgeCodec : S.Codec e (Dict Name Int)
peoplesAgeCodec =
    S.dict S.string S.int

set : Codec e comparable -> Codec e (Set comparable)

Codec for serializing a Set

tuple : Codec e a -> Codec e b -> Codec e ( a, b )

Codec for serializing a tuple with 2 elements

import Serialize as S

pointCodec : S.Codec e ( Float, Float )
pointCodec =
    S.tuple S.float S.float

triple : Codec e a -> Codec e b -> Codec e c -> Codec e ( a, b, c )

Codec for serializing a tuple with 3 elements

import Serialize as S

pointCodec : S.Codec e ( Float, Float, Float )
pointCodec =
    S.tuple S.float S.float S.float

result : Codec e error -> Codec e value -> Codec e (Result error value)

Codec for serializing a Result

enum : a -> List a -> Codec e a

A codec for serializing an item from a list of possible items. If you try to encode an item that isn't in the list then the first item is defaulted to.

import Serialize as S

type DaysOfWeek
    = Monday
    | Tuesday
    | Wednesday
    | Thursday
    | Friday
    | Saturday
    | Sunday

daysOfWeekCodec : S.Codec e DaysOfWeek
daysOfWeekCodec =
    S.enum Monday [ Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday ]

Note that inserting new items in the middle of the list or removing items is a breaking change. It's safe to add items to the end of the list though.

Records


type RecordCodec e a b

A partially built Codec for a record.

record : b -> RecordCodec e a b

Start creating a codec for a record.

import Serialize as S

type alias Point =
    { x : Int
    , y : Int
    }

pointCodec : S.Codec e Point
pointCodec =
    S.record Point
        -- Note that adding, removing, or reordering fields will prevent you from decoding any data you've previously encoded.
        |> S.field .x S.int
        |> S.field .y S.int
        |> S.finishRecord

field : (a -> f) -> Codec e f -> RecordCodec e a (f -> b) -> RecordCodec e a b

Add a field to the record we are creating a codec for.

finishRecord : RecordCodec e a a -> Codec e a

Finish creating a codec for a record.

Custom Types


type CustomTypeCodec a e match v

A partially built codec for a custom type.

customType : match -> CustomTypeCodec { youNeedAtLeastOneVariant : () } e match value

Starts building a Codec for a custom type. You need to pass a pattern matching function, see the FAQ for details.

import Serialize as S

type Semaphore
    = Red Int String Bool
    | Yellow Float
    | Green

semaphoreCodec : S.Codec e Semaphore
semaphoreCodec =
    S.customType
        (\redEncoder yellowEncoder greenEncoder value ->
            case value of
                Red i s b ->
                    redEncoder i s b

                Yellow f ->
                    yellowEncoder f

                Green ->
                    greenEncoder
        )
        -- Note that removing a variant, inserting a variant before an existing one, or swapping two variants will prevent you from decoding any data you've previously encoded.
        |> S.variant3 Red S.int S.string S.bool
        |> S.variant1 Yellow S.float
        |> S.variant0 Green
        -- It's safe to add new variants here later though
        |> S.finishCustomType

variant0 : v -> CustomTypeCodec z e (VariantEncoder -> a) v -> CustomTypeCodec () e a v

Define a variant with 0 parameters for a custom type.

variant1 : (a -> v) -> Codec error a -> CustomTypeCodec z error ((a -> VariantEncoder) -> b) v -> CustomTypeCodec () error b v

Define a variant with 1 parameters for a custom type.

variant2 : (a -> b -> v) -> Codec error a -> Codec error b -> CustomTypeCodec z error ((a -> b -> VariantEncoder) -> c) v -> CustomTypeCodec () error c v

Define a variant with 2 parameters for a custom type.

variant3 : (a -> b -> c -> v) -> Codec error a -> Codec error b -> Codec error c -> CustomTypeCodec z error ((a -> b -> c -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v

Define a variant with 3 parameters for a custom type.

variant4 : (a -> b -> c -> d -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> CustomTypeCodec z error ((a -> b -> c -> d -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v

Define a variant with 4 parameters for a custom type.

variant5 : (a -> b -> c -> d -> e -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v

Define a variant with 5 parameters for a custom type.

variant6 : (a -> b -> c -> d -> e -> f -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> Codec error f -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> f -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v

Define a variant with 6 parameters for a custom type.

variant7 : (a -> b -> c -> d -> e -> f -> g -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> Codec error f -> Codec error g -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> f -> g -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v

Define a variant with 7 parameters for a custom type.

variant8 : (a -> b -> c -> d -> e -> f -> g -> h -> v) -> Codec error a -> Codec error b -> Codec error c -> Codec error d -> Codec error e -> Codec error f -> Codec error g -> Codec error h -> CustomTypeCodec z error ((a -> b -> c -> d -> e -> f -> g -> h -> VariantEncoder) -> partial) v -> CustomTypeCodec () error partial v

Define a variant with 8 parameters for a custom type.

finishCustomType : CustomTypeCodec () e (a -> VariantEncoder) a -> Codec e a

Finish creating a codec for a custom type.


type VariantEncoder

Mapping

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

Map from one codec to another codec

import Serialize as S

type UserId
    = UserId Int

userIdCodec : S.Codec e UserId
userIdCodec =
    S.int |> S.map UserId (\(UserId id) -> id)

Note that there's nothing preventing you from encoding Elm values that will map to some different value when you decode them. I recommend writing tests for Codecs that use map to make sure you get back the same Elm value you put in. Here's some helper functions to get you started.

mapValid : (a -> Result e b) -> (b -> a) -> Codec e a -> Codec e b

Map from one codec to another codec in a way that can potentially fail when decoding.

-- Email module is from https://package.elm-lang.org/packages/tricycle/elm-email/1.0.2/


import Email
import Serialize as S

emailCodec : S.Codec String Float
emailCodec =
    S.string
        |> S.mapValid
            (\text ->
                case Email.fromString text of
                    Just email ->
                        Ok email

                    Nothing ->
                        Err "Invalid email"
            )
            Email.toString

Note that there's nothing preventing you from encoding Elm values that will produce Err when you decode them. I recommend writing tests for Codecs that use mapValid to make sure you get back the same Elm value you put in. Here's some helper functions to get you started.

mapError : (e1 -> e2) -> Codec e1 a -> Codec e2 a

Map errors generated by mapValid.

Stack unsafe

lazy : (() -> Codec e a) -> Codec e a

Handle situations where you need to define a codec in terms of itself.

import Serialize as S

type Peano
    = Peano (Maybe Peano)

{-| The compiler will complain that this function causes an infinite loop.
-}
badPeanoCodec : S.Codec e Peano
badPeanoCodec =
    S.maybe badPeanoCodec |> S.map Peano (\(Peano a) -> a)

{-| Now the compiler is happy!
-}
goodPeanoCodec : S.Codec e Peano
goodPeanoCodec =
    S.maybe (S.lazy (\() -> goodPeanoCodec)) |> S.map Peano (\(Peano a) -> a)

Warning: This is not stack safe.

In general if you have a type that contains itself, like with our the Peano example, then you're at risk of a stack overflow while decoding. Even if you're translating your nested data into a list before encoding, you're at risk, because the function translating back after decoding can cause a stack overflow if the original value was nested deeply enough. Be careful here, and test your codecs using elm-test with larger inputs than you ever expect to see in real life.