canceraiddev / elm-pages / OptimizedDecoder

This module allows you to build decoders that elm-pages can optimize for you in your StaticHttp requests. It does this by stripping of unused fields during the CLI build step. When it runs in production, it will just run a plain elm/json decoder, so you're fetching and decoding the stripped-down data, but without any performance penalty.

For convenience, this library also includes a Json.Decode.Exploration.Pipeline module which is largely a copy of NoRedInk/elm-decode-pipeline.

Dealing with warnings and errors


type alias Error =
Json.Decode.Error

A simple type alias for Json.Decode.Error.

errorToString : Json.Decode.Error -> String

A simple wrapper for Json.Decode.errorToString.

Primitives


type alias Decoder a =
Internal.OptimizedDecoder a

A decoder that will be optimized in your production bundle.

string : Decoder String

Decode a string.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" "hello world" """
    |> decodeString string
--> Success "hello world"


""" 123 """
    |> decodeString string
--> Errors (Nonempty (Here <| Expected TString (Encode.int 123)) [])

bool : Decoder Basics.Bool

Decode a boolean value.

""" [ true, false ] """
    |> decodeString (list bool)
--> Success [ True, False ]

int : Decoder Basics.Int

Decode a number into an Int.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" 123 """
    |> decodeString int
--> Success 123


""" 0.1 """
    |> decodeString int
--> Errors <|
-->   Nonempty
-->     (Here <| Expected TInt (Encode.float 0.1))
-->     []

float : Decoder Basics.Float

Decode a number into a Float.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" 12.34 """
    |> decodeString float
--> Success 12.34


""" 12 """
    |> decodeString float
--> Success 12


""" null """
    |> decodeString float
--> Errors (Nonempty (Here <| Expected TNumber Encode.null) [])


type alias Value =
Json.Decode.Value

A simple type alias for Json.Decode.Value.

Data Structures

nullable : Decoder a -> Decoder (Maybe a)

Decodes successfully and wraps with a Just. If the values is null succeeds with Nothing.

""" [ { "foo": "bar" }, { "foo": null } ] """
    |> decodeString (list <| field "foo" <| nullable string)
--> Success [ Just "bar", Nothing ]

list : Decoder a -> Decoder (List a)

Decode a list of values, decoding each entry with the provided decoder.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" [ "foo", "bar" ] """
    |> decodeString (list string)
--> Success [ "foo", "bar" ]


""" [ "foo", null ] """
    |> decodeString (list string)
--> Errors <|
-->   Nonempty
-->     (AtIndex 1 <|
-->       Nonempty (Here <| Expected TString Encode.null) []
-->     )
-->     []

array : Decoder a -> Decoder (Array a)

Convenience function. Decode a JSON array into an Elm Array.

import Array

""" [ 1, 2, 3 ] """
    |> decodeString (array int)
--> Success <| Array.fromList [ 1, 2, 3 ]

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

Convenience function. Decode a JSON object into an Elm Dict String.

import Dict


""" { "foo": "bar", "bar": "hi there" } """
    |> decodeString (dict string)
--> Success <| Dict.fromList
-->   [ ( "bar", "hi there" )
-->   , ( "foo", "bar" )
-->   ]

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

Decode a JSON object into a list of key-value pairs. The decoder you provide will be used to decode the values.

""" { "foo": "bar", "hello": "world" } """
    |> decodeString (keyValuePairs string)
--> Success [ ( "foo", "bar" ), ( "hello", "world" ) ]

Object Primitives

field : String -> Decoder a -> Decoder a

Decode the content of a field using a provided decoder.

import List.Nonempty as Nonempty
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode

""" { "foo": "bar" } """
    |> decodeString (field "foo" string)
--> Success "bar"


""" [ { "foo": "bar" }, { "foo": "baz", "hello": "world" } ] """
    |> decodeString (list (field "foo" string))
--> WithWarnings expectedWarnings [ "bar", "baz" ]


expectedWarnings : Warnings
expectedWarnings =
    UnusedField "hello"
        |> Here
        |> Nonempty.fromElement
        |> AtIndex 1
        |> Nonempty.fromElement

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

Decodes a value at a certain path, using a provided decoder. Essentially, writing at [ "a", "b", "c" ] string is sugar over writing field "a" (field "b" (field "c" string))}.

""" { "a": { "b": { "c": "hi there" } } } """
    |> decodeString (at [ "a", "b", "c" ] string)
--> Success "hi there"

index : Basics.Int -> Decoder a -> Decoder a

Decode a specific index using a specified Decoder.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" [ "hello", 123 ] """
    |> decodeString (map2 Tuple.pair (index 0 string) (index 1 int))
--> Success ( "hello", 123 )


""" [ "hello", "there" ] """
    |> decodeString (index 1 string)
--> WithWarnings (Nonempty (AtIndex 0 (Nonempty (Here (UnusedValue (Encode.string "hello"))) [])) [])
-->   "there"

optionalField : String -> Decoder a -> Decoder (Maybe a)

If a field is missing, succeed with Nothing. If it is present, decode it as normal and wrap successes in a Just.

When decoding with maybe, if a field is present but malformed, you get a success and Nothing. optionalField gives you a failed decoding in that case, so you know you received malformed data.

Examples:

import Json.Decode exposing (..)
import Json.Encode

Let's define a stuffDecoder that extracts the "stuff" field, if it exists.

stuffDecoder : Decoder (Maybe String)
stuffDecoder =
    optionalField "stuff" string

If the "stuff" field is missing, decode to Nothing.

""" { } """
    |> decodeString stuffDecoder
--> Ok Nothing

If the "stuff" field is present but not a String, fail decoding.

expectedError : Error
expectedError =
    Failure "Expecting a STRING" (Json.Encode.list identity [])
      |> Field "stuff"

""" { "stuff": [] } """
    |> decodeString stuffDecoder
--> Err expectedError

If the "stuff" field is present and valid, decode to Just String.

""" { "stuff": "yay!" } """
    |> decodeString stuffDecoder
--> Ok <| Just "yay!"

Definition from the json-extra package: https://github.com/elm-community/json-extra.

Inconsistent Structure

maybe : Decoder a -> Decoder (Maybe a)

Decodes successfully and wraps with a Just, handling failure by succeeding with Nothing.

import List.Nonempty as Nonempty
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" [ "foo", 12 ] """
    |> decodeString (list <| maybe string)
--> WithWarnings expectedWarnings [ Just "foo", Nothing ]


expectedWarnings : Warnings
expectedWarnings =
    UnusedValue (Encode.int 12)
        |> Here
        |> Nonempty.fromElement
        |> AtIndex 1
        |> Nonempty.fromElement

oneOf : List (Decoder a) -> Decoder a

Tries a bunch of decoders. The first one to not fail will be the one used.

If all fail, the errors are collected into a BadOneOf.

import List.Nonempty as Nonempty
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode

""" [ 12, "whatever" ] """
    |> decodeString (list <| oneOf [ map String.fromInt int, string ])
--> Success [ "12", "whatever" ]


""" null """
    |> decodeString (oneOf [ string, map String.fromInt int ])
--> Errors <| Nonempty.fromElement <| Here <| BadOneOf
-->   [ Nonempty.fromElement <| Here <| Expected TString Encode.null
-->   , Nonempty.fromElement <| Here <| Expected TInt Encode.null
-->   ]

Fancy Decoding

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

Required when using (mutually) recursive decoders.

value : Decoder Value

Extract a piece without actually decoding it.

If a structure is decoded as a value, everything in the structure will be considered as having been used and will not appear in UnusedValue warnings.

import Json.Encode as Encode


""" [ 123, "world" ] """
    |> decodeString value
--> Success (Encode.list identity [ Encode.int 123, Encode.string "world" ])

null : a -> Decoder a

Decode a null and succeed with some value.

""" null """
    |> decodeString (null "it was null")
--> Success "it was null"

Note that undefined and null are not the same thing. This cannot be used to verify that a field is missing, only that it is explicitly set to null.

""" { "foo": null } """
    |> decodeString (field "foo" (null ()))
--> Success ()


import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" { } """
    |> decodeString (field "foo" (null ()))
--> Errors <|
-->   Nonempty
-->     (Here <| Expected (TObjectField "foo") (Encode.object []))
-->     []

succeed : a -> Decoder a

A decoder that will ignore the actual JSON and succeed with the provided value. Note that this may still fail when dealing with an invalid JSON string.

If a value in the JSON ends up being ignored because of this, this will cause a warning.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode


""" null """
    |> decodeString (value |> andThen (\_ -> succeed "hello world"))
--> Success "hello world"


""" null """
    |> decodeString (succeed "hello world")
--> WithWarnings
-->     (Nonempty (Here <| UnusedValue Encode.null) [])
-->     "hello world"


""" foo """
    |> decodeString (succeed "hello world")
--> BadJson

fail : String -> Decoder a

Ignore the json and fail with a provided message.

import List.Nonempty exposing (Nonempty(..))
import Json.Decode.Exploration.Located exposing (Located(..))
import Json.Encode as Encode

""" "hello" """
    |> decodeString (fail "failure")
--> Errors (Nonempty (Here <| Failure "failure" (Just <| Encode.string "hello")) [])

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

Chain decoders where one decoder depends on the value of another decoder.

Mapping

Note: If you run out of map functions, take a look at the pipeline module which makes it easier to handle large objects.

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

Useful for transforming decoders.

""" "foo" """
    |> decodeString (map String.toUpper string)
--> Success "FOO"

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

Combine 2 decoders.

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

Combine 3 decoders.

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

Combine 4 decoders.

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

Combine 5 decoders.

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

Combine 6 decoders.

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

Combine 7 decoders.

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

Combine 8 decoders.

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

Decode an argument and provide it to a function in a decoder.

decoder : Decoder String
decoder =
    succeed (String.repeat)
        |> andMap (field "count" int)
        |> andMap (field "val" string)


""" { "val": "hi", "count": 3 } """
    |> decodeString decoder
--> Success "hihihi"

fromResult : Result String value -> Decoder value

Turn a Result into a Decoder (uses succeed and fail under the hood). This is often helpful for chaining with andThen.

Directly Running Decoders

Usually you'll be passing your decoders to

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

A simple wrapper for Json.Decode.errorToString.

This will directly call the raw elm/json decoder that is stored under the hood.

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

A simple wrapper for Json.Decode.errorToString.

This will directly call the raw elm/json decoder that is stored under the hood.

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

Usually you'll want to directly pass your OptimizedDecoder to StaticHttp or other elm-pages APIs. But if you want to re-use your decoder somewhere else, it may be useful to turn it into a plain elm/json decoder.