ryannhg / graphql / GraphQL.Decode


type Decoder value

When you make a GraphQL request, you'll get a JSON response like this one:

{
  "data": {
    "hero": {
      "name": "R2-D2",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}

This Decoder type represents a value that knows how to convert a raw JSON response into standard Elm values that can be used in your Elm application.

Learning and troubleshooting

When I first got started with JSON decoders, I had a hard time visualizing what was going on. For that reason, this module comes with a test function to help you easily verify your code is behaving the way you expect.

This is inspired by the elm/json package's decodeString function, which is great for testing JSON decoders when you get stuck!

test : { decoder : Decoder value, json : String } -> Result Json.Decode.Error value

This function is exposed so you can quickly check if your GraphQL Decoder works as you expect with a raw JSON string.

import GraphQL.Decode

testResult : Result Http.Error Int
testResult =
    GraphQL.Decode.test
        { decoder = GraphQL.Decode.int
        , json = """ 1000 """"
        }

Scalars

In GraphQL, scalars are strings, numbers, IDs, and other basic primitives that your API can send. For the five built-in scalars, these functions are provided below.

See the scalar function to work with custom scalars specific to your API.

string : Decoder String

A decoder for the built-in String scalar.

import GraphQL.Decode

GraphQL.Decode.test
    { decoder = GraphQL.Decode.string
    , json = """ "Hello, world!" """
    }
    == Ok "Hello, world!"

float : Decoder Basics.Float

A decoder for the built-in Float scalar.

import GraphQL.Decode

GraphQL.Decode.test
    { decoder = GraphQL.Decode.float
    , json = """ 1.25 """
    }
    == Ok 1.25

int : Decoder Basics.Int

A decoder for the built-in Int scalar.

import GraphQL.Decode

GraphQL.Decode.test
    { decoder = GraphQL.Decode.int
    , json = """ 9000 """
    }
    == Ok 9000

bool : Decoder Basics.Bool

A decoder for the built-in Boolean scalar.

import GraphQL.Decode

GraphQL.Decode.test
    { decoder = GraphQL.Decode.bool
    , json = """ true """
    }
    == Ok True

id : Decoder GraphQL.Scalar.Id.Id

A decoder for the built-in ID scalar.

Uses GraphQL.Scalar.Id as a way to prevent mixing up String and Id values.

import GraphQL.Decode

-- Works with JSON strings
GraphQL.Decode.test
    { decoder = GraphQL.Decode.id
    , json = """ "abc" """
    }
    == Ok (Id "abc")

-- Works with JSON numbers
test
    { decoder = id
    , json = """ 12345 """
    }
    == Ok (Id "12345")

scalar : Json.Decode.Decoder scalar -> Decoder scalar

GraphQL allows APIs to define custom scalar values, like Date or Currency. This function allows you to work with those custom values.

Here's an example of using this function to work with a DateTime type. We recommend defining each custom scalar in a separate Elm module, like GraphQL.Scalar.DateTime:

-- module GraphQL.Scalar.DateTime exposing
--     ( DateTime
--     , decoder, encode
--     )


import GraphQL.Decode exposing (Decoder)
import Iso8601
import Time

type alias DateTime =
    Time.Posix

decoder : Decoder DateTime
decoder =
    GraphQL.Decode.scalar Iso8601.decoder

The snippet above uses elm/time and rtfeldman/elm-iso8601-date-strings to convert a String value from our API into a Time.Posix value.

import GraphQL.Decode
import GraphQL.Scalar.DateTime

GraphQL.Decode.test
    { decoder = GraphQL.Scalar.DateTime.decoder
    , value = """ "2022-12-26T01:45:56.520Z" """
    }
    == Ok (Posix 1672019156520)

Enums

enum : List ( String, enum ) -> Decoder enum

A decoder for handling enum values. It allows you to specify a list of allowed enums that can come back with a certain request.

To prevent code duplication, we recommend defining one decoder for each enum in a separate Elm module, like GraphQL.Enum.Episode. Here's how you can use this function to define a decoder for the Episode enum type:

import GraphQL.Decode exposing (Decoder)

type Episode
    = NewHope
    | EmpireStrikesBack
    | ReturnOfTheJedi

decoder : Decoder Episode
decoder =
    GraphQL.Decode.enum
        [ ( "NEWHOPE", NewHope )
        , ( "EMPIRE", EmpireStrikesBack )
        , ( "RETURN", ReturnOfTheJedi )
        ]

Note: If an enum is missing from that list, this package will return a JSON decoding error letting you know which enum variant was missing.

Objects

object : (field -> object) -> Decoder (field -> object)

A decoder for working with object types. This function works in conjunction with field to allow you to safely decode one field at a time.

import GraphQL.Decode exposing (Decoder)

type alias Person =
    { id : Id
    , fullName : String
    }

decoder : Decoder Person
decoder =
    GraphQL.Decode.object Person
        |> GraphQL.Decode.field
            { name = "id"
            , decoder = GraphQL.Decode.id
            }
        |> GraphQL.Decode.field
            { name = "fullName"
            , decoder = GraphQL.Decode.string
            }

field : { name : String, decoder : Decoder field } -> Decoder (field -> output) -> Decoder output

When used with object, this function allows you to add fields to an object.

Note: The order of the fields in the type alias need to match the order you add the fields in your decoder.

import GraphQL.Decode exposing (Decoder)
import GraphQL.Enum.Episode

type alias Jedi =
    { name : String
    , appearsIn : List GraphQL.Enum.Episode
    }

decoder : Decoder Person
decoder =
    GraphQL.Decode.object Person
        |> GraphQL.Decode.field
            { name = "name"
            , decoder = GraphQL.Decode.string
            }
        |> GraphQL.Decode.field
            { name = "appearsIn"
            , decoder =
                GraphQL.Decode.list
                    GraphQL.Enum.Episode.decoder
            }

Lists & Nullable Fields

maybe : Decoder value -> Decoder (Maybe value)

Use this function to tell an existing decoder that we expect a nullable value, which might not guaranteed to be available:

import GraphQL.Decode exposing (Decoder)

stringDecoder : Decoder String
stringDecoder =
    GraphQL.Decoder.string

nullableStringDecoder : Decoder (Maybe String)
nullableStringDecoder =
    GraphQL.Decoder.maybe GraphQL.Decoder.string

list : Decoder value -> Decoder (List value)

Use this function to tell an existing decoder that we expect a list of values, rather than a single value:

import GraphQL.Decode exposing (Decoder)

stringDecoder : Decoder String
stringDecoder =
    GraphQL.Decoder.string

listOfStringDecoder : Decoder (List String)
listOfStringDecoder =
    GraphQL.Decoder.list GraphQL.Decoder.string

Interfaces & Union Types

❗️ Always include __typename

Interfaces and union type decoders require the __typename field in the selection. Without it, Elm won't be able to match on the typename provided.

Here's an example from the official GraphQL documentation:

{
  search(text: "an") {
    __typename
    ... on Human {
      name
      height
    }
    ... on Droid {
      name
      primaryFunction
    }
    ... on Starship {
      name
      length
    }
  }
}

Because the hero query returns an interface, you should always include __typename in your selection set. This will allow your Elm frontend to know which types came back:

{
  "data": {
    "search": [
      {
        "__typename": "Human",
        "name": "Han Solo",
        "height": 1.8
      },
      {
        "__typename": "Human",
        "name": "Leia Organa",
        "height": 1.5
      },
      {
        "__typename": "Starship",
        "name": "TIE Advanced x1",
        "length": 9.2
      }
    ]
  }
}

union : List (Variant union) -> Decoder union

Decoder for a union type. Use this with the variant function to select variants by typename:

import GraphQL.Decode exposing (Decoder)

type SearchUnion
    = On_Human Human
    | On_Droid Droid
    | On_Starship Starship

decoder : Decoder SearchUnion
decoder =
    GraphQL.Decode.union
        [ GraphQL.Decode.variant
            { typename = "Human"
            , onVariant = On_Human
            , decoder = humanDecoder
            }
        , GraphQL.Decode.variant
            { typename = "Droid"
            , onVariant = On_Droid
            , decoder = droidDecoder
            }
        , GraphQL.Decode.variant
            { typename = "Starship"
            , onVariant = On_Starship
            , decoder = starshipDecoder
            }
        ]

Each variant will expect a decoder for the data you expect. This will involve using the object and field functions in this module:

type alias Human =
    { name : String
    , height : Int
    }

humanDecoder : Decoder Human
humanDecoder =
    GraphQL.Decode.object Human
        |> GraphQL.Decode.field
            { name = "name"
            , decoder = GraphQL.Decode.string
            }
        |> GraphQL.Decode.field
            { name = "height"
            , decoder = GraphQL.Decode.int
            }

type alias Droid =
    { name : String
    , primaryFunction : String
    }

droidDecoder : Decoder Droid
droidDecoder =
    GraphQL.Decode.object Droid
        |> GraphQL.Decode.field
            { name = "name"
            , decoder = GraphQL.Decode.string
            }
        |> GraphQL.Decode.field
            { name = "primaryFunction"
            , decoder = GraphQL.Decode.string
            }

type alias Starship =
    { name : String
    , length : Int
    }

starshipDecoder : Decoder Starship
starshipDecoder =
    GraphQL.Decode.object Starship
        |> GraphQL.Decode.field
            { name = "name"
            , decoder = GraphQL.Decode.string
            }
        |> GraphQL.Decode.field
            { name = "length"
            , decoder = GraphQL.Decode.int
            }

❗️ See the important note about using __typename to prevent issues

interface : List (Variant interface) -> Decoder interface

Decoder for interfaces that allow you to select extra fields. This works alongside the variant function to define which variants you are interested in tracking.

import GraphQL.Decode exposing (Decoder)

type HeroInterface
    = On_Human Human
    | On_Droid Droid

decoder : Decoder SearchUnion
decoder =
    GraphQL.Decode.interface
        [ GraphQL.Decode.variant
            { typename = "Human"
            , onVariant = On_Human
            , decoder = humanDecoder
            }
        , GraphQL.Decode.variant
            { typename = "Droid"
            , onVariant = On_Droid
            , decoder = droidDecoder
            }
        ]

You can see the union example to understand how to make humanDecoder or droidDecoder. Their implementations are identical!

❗️ See the important note about using __typename to prevent issues


type Variant value

A type that represents a custom type variant on a union type or interface. These are used with union or interface in the examples above.

variant : { typename : String, onVariant : object -> value, decoder : Decoder object } -> Variant value

A function intended to be used with union or interface for selecting specific custom type variants by typename.

import GraphQL.Decode exposing (Variant)

type HeroInterface
    = On_Human Human
    | On_Droid Droid

humanVariant : Variant HeroInterface
humanVariant =
    GraphQL.Decode.variant
        { typename = "Human"
        , onVariant = On_Human
        , decoder = humanDecoder
        }

droidVariant : Variant HeroInterface
droidVariant =
    GraphQL.Decode.variant
        { typename = "Droid"
        , onVariant = On_Droid
        , decoder = droidDecoder
        }

Internals

These functions are used internally by GraphQL.Http, and you won't need them in your projects.

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

Map a decoder of one value to another.

toJsonDecoder : Decoder value -> Json.Decode.Decoder value