MaybeJustJames / yaml / Yaml.Decode

Turn YAML into Elm values. The library is structured in a similar way to Json.Decode, so if you haven't worked with decoders before, reading through the guide may be helpful.

Table of Contents


type Decoder a

A value that knows how to decode YAML.

There is a whole section in guide.elm-lang.org about decoders, so check it out for a more comprehensive introduction!

Run Decoders

fromString : Decoder a -> String -> Result Error a

Decode a given string into an Elm value based on the provided Decoder. This will fail if the string is not well-formed YAML or if the Decoder doesn't match the input.

fromString int "4" --> Ok 4

fromString int "hello" --> Err (Decoding "Expected int, got: \"hello\" (string)")


type alias Value =
Yaml.Parser.Value

Represents a YAML tree.


type Error
    = Parsing String
    | Decoding String

A structured error describing how a decoder failed.

fromValue : Decoder a -> Value -> Result Error a

Run a Decoder on a Yaml Value.

errorToString : Error -> String

Convert a structured error into a String that is nice for debugging.

Primitives

string : Decoder String

Decode a YAML string into an Elm String.

fromString string "true"
--> Err (Decoding "Expected string, got: True (bool)")

fromString string "'true'" --> Ok "true"

fromString string "42"
--> Err (Decoding "Expected string, got: 42 (int)")

fromString string "'42'" --> Ok "42"

fromString string "hello" --> Ok "hello"

fromString string "hello: 42"
--> Err (Decoding "Expected string, got: { hello: 42 (int) } (map)")

bool : Decoder Basics.Bool

Decode a YAML boolean into an Elm Bool.

fromString bool "true" --> Ok True

fromString bool "'true'"
--> Err (Decoding "Expected bool, got: \"true\" (string)")

fromString bool "42"
--> Err (Decoding "Expected bool, got: 42 (int)")

fromString bool "hello"
--> Err (Decoding "Expected bool, got: \"hello\" (string)")

fromString bool "hello: 42"
--> Err (Decoding "Expected bool, got: { hello: 42 (int) } (map)")

int : Decoder Basics.Int

Decode a YAML number into an Elm Int.

fromString int "true"
--> Err (Decoding "Expected int, got: True (bool)")

fromString int "'true'"
--> Err (Decoding "Expected int, got: \"true\" (string)")

fromString int "42"        --> Ok 42

fromString int "'42'"
--> Err (Decoding "Expected int, got: \"42\" (string)")

fromString int "3.14"
--> Err (Decoding "Expected int, got: 3.14 (float)")

fromString int "hello"
--> Err (Decoding "Expected int, got: \"hello\" (string)")

fromString int "hello: 42"
--> Err (Decoding "Expected int, got: { hello: 42 (int) } (map)")

float : Decoder Basics.Float

Decode a YAML number into an Elm Float.

fromString float "true"
--> Err (Decoding "Expected float, got: True (bool)")

fromString float "'true'"
--> Err (Decoding "Expected float, got: \"true\" (string)")

fromString float "42" --> Ok 42

fromString float "'42'"
--> Err (Decoding "Expected float, got: \"42\" (string)")

fromString float "3.14" --> Ok 3.14

fromString float "hello"
--> Err (Decoding "Expected float, got: \"hello\" (string)")

fromString float "hello: 42"
--> Err (Decoding "Expected float, got: { hello: 42 (int) } (map)")

null : Decoder (Maybe a)

Decode a YAML null value into Nothing.

fromString null "" --> Ok Nothing

fromString null "null" --> Ok Nothing

fromString null "true"
--> Err (Decoding "Expected null, got: True (bool)")

fromString null "42"
--> Err (Decoding "Expected null, got: 42 (int)")

fromString null "hello: 42"
--> Err (Decoding "Expected null, got: { hello: 42 (int) } (map)")

Data Structures

nullable : Decoder a -> Decoder (Maybe a)

Decode a nullable YAML value into an Elm value.

fromString (nullable int) "42" --> Ok (Just 42)

fromString (nullable int) "3.14"
--> Err (Decoding "Expected int, got: 3.14 (float)")

fromString (nullable int) "null" --> Ok Nothing

fromString (nullable int) "true"
--> Err (Decoding "Expected int, got: True (bool)")

list : Decoder a -> Decoder (List a)

Decode a YAML array into an Elm List.

fromString (list int) "[1,2,3]" --> Ok [ 1, 2, 3 ]

fromString (list bool) "[ true, false ]" --> Ok [ True, False ]

fromString (list float) "true"
--> Err (Decoding "Expected list, got: True (bool)")

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

Decode a YAML record into an Elm Dict.

import Dict exposing (Dict)

fromString (dict int) "{ alice: 42, bob: 99 }"
--> Ok (Dict.fromList [ ( "alice", 42 ), ( "bob", 99 ) ])

fromString (dict string) "42"
--> Err (Decoding "Expected record, got: 42 (int)")

Record Primitives

field : String -> Decoder a -> Decoder a

Decode a YAML record, requiring a particular field.

fromString (field "x" int) "{ x: 3 }" --> Ok 3

fromString (field "x" int) "{ x: 3, y: 4 }" --> Ok 3

fromString (field "x" int) "{ x: true }" --> Err (Decoding "Expected int, got: True (bool)")

fromString (field "x" int) "{ y: 4 }" --> Err (Decoding "Expected property: x")

fromString (field "name" string) "{ name: Tom }" --> Ok "Tom"

The record can have other fields. Lots of them! The only thing this decoder cares about is if x is present and that the value there can be decoded.

Check out mapN and andMap to see how to decode multiple fields!

at : List String -> Decoder a -> Decoder a

Decode a nested YAML record, requiring certain fields.

yaml : String
yaml = """{ person: { name: Tom, age: 42 } }"""

fromString (at ["person", "name"] string) yaml --> Ok "Tom"

fromString (at ["person", "age"] int) yaml     --> Ok 42

fromString (at ["not", "here"] int) yaml --> Err (Decoding "Expected property: not")

This is really just shorthand for field. Equivalent to saying things like:

fromString (field "person" (field "name" string)) yaml
--> fromString (at [ "person", "name" ] string) yaml

Inconsistent Structure

oneOf : List (Decoder a) -> Decoder a

Try a list of different decoders. Pick the first working one. This can be useful if the YAML comes in different formats. For example, if you want to read an array of numbers but some of them are null.

fromString (oneOf [ field "a" int, field "b" int ]) "{a: 42}" --> Ok 42

fromString (oneOf [ field "a" int, field "b" int ]) "{b: 3}" --> Ok 3

fromString (oneOf [ field "a" int, field "b" int ]) "true" --> Err (Decoding "Empty")

maybe : Decoder a -> Decoder (Maybe a)

Makes its argument optional. A decoder which returns Nothing when it fails.

Helpful when dealing with optional fields: you probably want to use maybe outside field or at. Here are a few examples:

yaml : String
yaml = "{ name: Stacy, age: 27, temperature: 37.6 }"

fromString (maybe (field "age" int)) yaml      --> Ok (Just 27)

fromString (maybe (field "height" float)) yaml --> Ok Nothing

These two examples decode to Nothing if a field does not exist. They say there may be an age field, if it exists it must be an integer. And there may be a height field, if it exists it must be a Float.

You can also decode to Nothing if a field is a different type:

fromString (field "temperature" (maybe string)) yaml --> Ok Nothing

fromString (field "age" (maybe int)) yaml --> Ok (Just 27)

These two examples say you must have temperature and age fields and the content may be integers.

Maps

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

Transform the result of a decoder. For example, get the length of a string:

stringLength : Decoder Int
stringLength =
  map String.length string

fromString stringLength "hello" --> Ok 5

map runs the decoder (string in the example above) and gives the result to the function (String.length in the example above).

map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c

Try two decoders and then combine the result. You can use this to decode records with 2 fields:

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

point : Decoder Point
point =
  map2 Point
     (field "x" float)
     (field "y" float)

fromString point "{x: 1.2, y: 2.5}" --> Ok { x = 1.2, y = 2.5 }

map2 runs each decoder in order and privides the results to the function (taking 2 arguments; the Point constructor in the example above).

map3 : (a -> b -> c -> d) -> Decoder a -> Decoder b -> Decoder c -> Decoder d

Try three decoders and then combine the result.

map4 : (a -> b -> c -> d -> e) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e

Try four decoders and then combine the result.

map5 : (a -> b -> c -> d -> e -> f) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f

Try five decoders and then combine the result.

map6 : (a -> b -> c -> d -> e -> f -> g) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g

Try six decoders and then combine the result.

map7 : (a -> b -> c -> d -> e -> f -> g -> h) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h

Try seven decoders and then combine the result.

map8 : (a -> b -> c -> d -> e -> f -> g -> h -> i) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f -> Decoder g -> Decoder h -> Decoder i

Try eight decoders and then combine the result.

andMap : Decoder a -> Decoder (a -> b) -> Decoder b

Can be useful when decoding large objects incrementally.

For example, this can be used to to mapN for any number of arguments. See the documentation for Json.Decode.Extra.andMap for a thorough explanation of how to use andMap.

-- map4
sumFields : Decoder Int
sumFields =
  succeed (\a b c d -> a + b + c + d)
    |> andMap (field "a" int)
    |> andMap (field "b" int)
    |> andMap (field "c" int)
    |> andMap (field "d" int)

fromString sumFields "{a: 2, b: 5, c: -3, d: 9}" --> Ok 13

Fancy Decoding

lazy : (() -> Decoder a) -> Decoder a

Decode YAML with a recursive (nested) structure.

An example is nested comments:

type alias Comment =
  { comment : String
  , responses : Responses
  }

type Responses = Responses (List Comment)

comment : Decoder Comment
comment =
  map2 Comment
    (field "comment" string)
    (field "responses" (map Responses (list (lazy (\_ -> comment)))))

yaml : String
yaml = "{ comment: 'hello world', responses: [ {comment: 'hello', responses: [] } ] }"

fromString comment yaml
--> Ok { comment = "'hello world'", responses = Responses [{ comment = "'hello'", responses = Responses [] }] }

By using lazy you make sure that the decoder only expands to be as deep as the YAML structure. You can read more about recursive data structures here.

value : Decoder Value

Do not do anything with a YAML value, just bring it into Elm as a Value. This can be useful if you have particularly complex data that you would like to deal with later. Or if you are going to send it out of a port and do not care about its structure.

fail : String -> Decoder a

Ignore the YAML and make the decoder fail. This is handy when used with oneOf or andThen where you want to give a custom error message in some case.

See the andThen docs for an example.

succeed : a -> Decoder a

Ignore the YAML and produce a given Elm value.

fromString (succeed 42) "true" --> Ok 42

fromString (succeed 42) "[]" --> Ok 42

fromString (succeed 42) "{ " |> Result.toMaybe --> Nothing -- Long parsing error

andThen : (a -> Decoder b) -> Decoder a -> Decoder b

Create decoders that depend on previous results.

For example, if you decoding depends on a version field:

type alias Info =
    { data: Int }

info : Decoder Info
info =
    field "version" int
        |> andThen infoHelp

-- infoHelp takes the "version" integer as its argument
infoHelp : Int -> Decoder Info
infoHelp version =
    case version of
        4 ->
            infoDecoder4
        3 ->
            infoDecoder3
        _ ->
            fail ("Version "
                    ++ String.fromInt version
                    ++ " is not supported.")

infoDecoder4 : Decoder Info
infoDecoder4 =
    map Info (field "v4data" int)

infoDecoder3 : Decoder Info
infoDecoder3 =
    map (Info << String.length) (field "v3data" string)

fromString info "{ version: 3, v3data: test }"
--> Ok (Info 4)

fromString info "{ version: 4, v4data: 5 }"
--> Ok (Info 5)

fromString info "{ version: 1 }"
--> Err (Decoding "Version 1 is not supported.")

Conversions

fromResult : Result String a -> Decoder a

Convert a Result into a decoder.

This can be useful when you want to use functions that operate on Result in decoders, in combination with andThen.

validate : String -> Result String String
validate s =
    case s of
        "" -> Err "is empty"
        _ -> Ok s

fromString (string |> andThen (validate >> fromResult)) "\"not empty\"" --> Ok "not empty"

fromString (string |> andThen (validate >> fromResult)) "" --> Err (Decoding "is empty")

fromMaybe : String -> Maybe a -> Decoder a

Convert a Maybe into a decoder.

This can be useful when you want to use functions that operate on Maybe in decoders, in combination with andThen.

fromString (string |> andThen (String.toInt >> fromMaybe "Not an int")) "\"42\"" --> Ok 42

fromString (string |> andThen (String.toInt >> fromMaybe "Not an int")) "" --> Err (Decoding "Not an int")