mochi/types

Type definition helpers for Code First GraphQL.

This module provides builders for creating GraphQL types from Gleam types with minimal boilerplate, plus helpers for Dynamic conversion.

Object Type Builder

let user_type = types.object("User")
  |> types.description("A user in the system")
  |> types.id("id", fn(u: User) { u.id })
  |> types.string("name", fn(u: User) { u.name })
  |> types.int("age", fn(u: User) { u.age })
  |> types.build(decode_user)

Enum Builder

let role_enum = types.enum_type("Role")
  |> types.value("ADMIN")
  |> types.value("USER")
  |> types.deprecated_value_with_reason("GUEST", "Use USER instead")
  |> types.build_enum

Dynamic Conversion Helpers

Use these when building DataLoader encoders or custom resolvers:

fn user_to_dynamic(u: User) -> Dynamic {
  types.record([
    types.field("id", u.id),
    types.field("name", u.name),
    #("profile", profile_to_dynamic(u.profile)),
    #("age", types.option(u.age)),  // Option(Int) -> null if None
  ])
}

Types

Builder for GraphQL enum types

pub type EnumBuilder {
  EnumBuilder(
    name: String,
    description: option.Option(String),
    values: List(EnumValue),
  )
}

Constructors

A mapping between a Gleam value and its GraphQL enum representation

pub type EnumMapping(a) {
  EnumMapping(
    gleam_value: a,
    graphql_name: String,
    description: option.Option(String),
  )
}

Constructors

  • EnumMapping(
      gleam_value: a,
      graphql_name: String,
      description: option.Option(String),
    )

An enum value

pub type EnumValue {
  EnumValue(
    name: String,
    description: option.Option(String),
    is_deprecated: Bool,
    deprecation_reason: option.Option(String),
  )
}

Constructors

Builder for GraphQL input object types

pub type InputBuilder {
  InputBuilder(
    name: String,
    description: option.Option(String),
    fields: List(InputField),
  )
}

Constructors

A field in the input builder

pub type InputField {
  InputField(
    name: String,
    description: option.Option(String),
    field_type: schema.FieldType,
    default_value: option.Option(dynamic.Dynamic),
  )
}

Constructors

Builder for creating GraphQL object types

pub type TypeBuilder(a) {
  TypeBuilder(
    name: String,
    description: option.Option(String),
    fields: List(TypeField(a)),
  )
}

Constructors

A field in the type builder

pub type TypeField(a) {
  TypeField(
    name: String,
    description: option.Option(String),
    field_type: schema.FieldType,
    extractor: fn(a) -> dynamic.Dynamic,
    is_deprecated: Bool,
    deprecation_reason: option.Option(String),
  )
  TypeFieldWithArgs(
    name: String,
    description: option.Option(String),
    field_type: schema.FieldType,
    args: List(schema.ArgumentDefinition),
    resolver: fn(
      a,
      dict.Dict(String, dynamic.Dynamic),
      schema.ExecutionContext,
    ) -> Result(dynamic.Dynamic, String),
    is_deprecated: Bool,
    deprecation_reason: option.Option(String),
  )
}

Constructors

Values

pub fn bool(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> Bool,
) -> TypeBuilder(a)

Add a boolean field

pub fn build(
  builder: TypeBuilder(a),
  decoder: fn(dynamic.Dynamic) -> Result(a, String),
) -> schema.ObjectType

Build the TypeBuilder into an ObjectType with a decoder

pub fn build_direct(
  builder: TypeBuilder(a),
) -> #(schema.ObjectType, fn(a) -> dynamic.Dynamic)

Generate an encoder function from a TypeBuilder

The encoder uses the same extractors defined for each field, so you don’t need to write the same field mappings twice.

Example

let user_builder = types.object("User")
  |> types.id("id", fn(u: User) { u.id })
  |> types.string("name", fn(u: User) { u.name })

let user_type = types.build(user_builder, decode_user)
let user_encoder = types.encoder(user_builder)

// user_encoder(User("1", "Alice")) produces:
// {"id": "1", "name": "Alice"}

Build the TypeBuilder into an ObjectType using identity coerce instead of a decoder. This eliminates the encode/decode roundtrip — no Dict is built per object and no Dict lookups are done per field. The encoder is just to_dynamic (a BEAM no-op).

Safe as long as the resolver returns values of type a and the extractors are defined for the same type a. Do not use when resolvers return pre-encoded Dicts.

pub fn build_enum(builder: EnumBuilder) -> schema.EnumType

Build the enum type

pub fn build_input(
  builder: InputBuilder,
) -> schema.InputObjectType

Build the input type into a schema InputObjectType

pub fn build_with_encoder(
  builder: TypeBuilder(a),
  decoder: fn(dynamic.Dynamic) -> Result(a, String),
) -> #(schema.ObjectType, fn(a) -> dynamic.Dynamic)

Build the TypeBuilder into an ObjectType and auto-generated encoder

This is the recommended way to build types - it generates both the schema type and an encoder function from the same field definitions, eliminating redundant code.

Example

pub type User {
  User(id: String, name: String, age: Int)
}

let #(user_type, user_encoder) = types.object("User")
  |> types.id("id", fn(u: User) { u.id })
  |> types.string("name", fn(u: User) { u.name })
  |> types.int("age", fn(u: User) { u.age })
  |> types.build_with_encoder(decode_user)

// Use user_type in schema, user_encoder in resolvers
query.query(
  name: "user",
  returns: schema.named_type("User"),
  resolve: fn(_ctx) { Ok(User("1", "Alice", 30)) },
  encode: user_encoder,  // Auto-generated!
)
pub fn date(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)
pub fn datetime(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)
pub fn deprecated(
  builder: TypeBuilder(a),
  reason: String,
) -> TypeBuilder(a)

Mark the most recently added field as deprecated with a reason.

pub fn deprecated_no_reason(
  builder: TypeBuilder(a),
) -> TypeBuilder(a)

Mark the most recently added field as deprecated without a reason.

pub fn deprecated_value(
  builder: EnumBuilder,
  name: String,
) -> EnumBuilder

Add a deprecated enum value

pub fn deprecated_value_with_reason(
  builder: EnumBuilder,
  name: String,
  reason: String,
) -> EnumBuilder

Add a deprecated enum value with reason

pub fn description(
  builder: TypeBuilder(a),
  desc: String,
) -> TypeBuilder(a)

Add description to type

pub fn email(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)
pub fn encoder(
  builder: TypeBuilder(a),
) -> fn(a) -> dynamic.Dynamic
pub fn enum_description(
  builder: EnumBuilder,
  desc: String,
) -> EnumBuilder

Add description to enum

pub fn enum_from_mappings(
  name: String,
  mappings: List(EnumMapping(a)),
) -> #(
  schema.EnumType,
  fn(a) -> String,
  fn(String) -> Result(a, String),
)

Build an enum type from mappings with bidirectional coercion functions

Returns a tuple of:

  • The EnumType for schema registration
  • A function to convert Gleam value to GraphQL string
  • A function to convert GraphQL string to Gleam value
pub type Status {
  Active
  Inactive
  Pending
}

let #(enum_type, to_graphql, from_graphql) = types.enum_from_mappings(
  "Status",
  [
    types.enum_mapping(Active, "ACTIVE"),
    types.enum_mapping(Inactive, "INACTIVE"),
    types.enum_mapping_with_desc(Pending, "PENDING", "Awaiting approval"),
  ],
)

// Use in schema
query.new()
  |> query.add_enum(enum_type)

// Convert values
to_graphql(Active) // -> "ACTIVE"
from_graphql("ACTIVE") // -> Ok(Active)
pub fn enum_from_mappings_with_desc(
  name: String,
  description: String,
  mappings: List(EnumMapping(a)),
) -> #(
  schema.EnumType,
  fn(a) -> String,
  fn(String) -> Result(a, String),
)

Build an enum type from mappings with description

pub fn enum_mapping(
  gleam_value: a,
  graphql_name: String,
) -> EnumMapping(a)

Create an enum mapping

pub fn enum_mapping_with_desc(
  gleam_value: a,
  graphql_name: String,
  desc: String,
) -> EnumMapping(a)

Create an enum mapping with description

pub fn enum_type(name: String) -> EnumBuilder

Create a new enum builder

pub fn field(
  name: String,
  value: a,
) -> #(String, dynamic.Dynamic)

Shorthand: wrap a value in to_dynamic with a field name

Example

types.record([
  types.field("id", user.id),
  types.field("name", user.name),
])
pub fn field_description(
  builder: TypeBuilder(a),
  desc: String,
) -> TypeBuilder(a)
pub fn field_with_args(
  builder: TypeBuilder(a),
  name name: String,
  returns field_type: schema.FieldType,
  args args: List(schema.ArgumentDefinition),
  desc description: String,
  resolve resolver: fn(
    a,
    dict.Dict(String, dynamic.Dynamic),
    schema.ExecutionContext,
  ) -> Result(dynamic.Dynamic, String),
) -> TypeBuilder(a)

Add a field with arguments and custom resolver

types.object("User")
  |> types.field_with_args(
    name: "posts",
    returns: schema.list_type(schema.named_type("Post")),
    args: [schema.arg("limit", schema.int_type())],
    desc: "User's posts with optional limit",
    resolve: fn(user, args, ctx) {
      let limit = query.get_optional_int(args, "limit")
      get_user_posts(user.id, limit)
    },
  )
pub fn float(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> Float,
) -> TypeBuilder(a)

Add a float field

pub fn id(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)

Add an ID field

pub fn input(name: String) -> InputBuilder

Create a new input type builder

let create_user_input = types.input("CreateUserInput")
  |> types.input_description("Input for creating a new user")
  |> types.input_string("name", "User's full name")
  |> types.input_string("email", "User's email address")
  |> types.input_optional_int("age", "User's age")
  |> types.build_input
pub fn input_bool(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add a required bool field to input

pub fn input_description(
  builder: InputBuilder,
  desc: String,
) -> InputBuilder

Add description to input type

pub fn input_field(
  builder: InputBuilder,
  name: String,
  field_type: schema.FieldType,
  desc: String,
) -> InputBuilder

Add a field with custom type to input

pub fn input_field_with_default(
  builder: InputBuilder,
  name: String,
  field_type: schema.FieldType,
  default: dynamic.Dynamic,
  desc: String,
) -> InputBuilder

Add a field with custom type and default value to input

pub fn input_float(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add a required float field to input

pub fn input_id(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add a required ID field to input

pub fn input_int(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add a required int field to input

pub fn input_optional_bool(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add an optional bool field to input

pub fn input_optional_float(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add an optional float field to input

pub fn input_optional_int(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add an optional int field to input

pub fn input_optional_string(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add an optional string field to input

pub fn input_string(
  builder: InputBuilder,
  name: String,
  desc: String,
) -> InputBuilder

Add a required string field to input

pub fn int(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> Int,
) -> TypeBuilder(a)

Add an int field

pub fn json(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> dynamic.Dynamic,
) -> TypeBuilder(a)
pub fn list_bool(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(Bool),
) -> TypeBuilder(a)
pub fn list_float(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(Float),
) -> TypeBuilder(a)
pub fn list_id(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(String),
) -> TypeBuilder(a)
pub fn list_int(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(Int),
) -> TypeBuilder(a)

Add a list of ints field

pub fn list_object(
  builder: TypeBuilder(a),
  name: String,
  type_name: String,
  extractor: fn(a) -> dynamic.Dynamic,
) -> TypeBuilder(a)

Add a list of related objects field

pub fn list_string(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(String),
) -> TypeBuilder(a)

Add a list of strings field

pub fn non_null_bool(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> Bool,
) -> TypeBuilder(a)

Add a non-null bool field

pub fn non_null_field(
  builder: TypeBuilder(a),
  name: String,
  field_type: schema.FieldType,
  extractor: fn(a) -> dynamic.Dynamic,
) -> TypeBuilder(a)

Add a non-null field

pub fn non_null_float(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> Float,
) -> TypeBuilder(a)

Add a non-null float field

pub fn non_null_int(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> Int,
) -> TypeBuilder(a)

Add a non-null int field

pub fn non_null_list_float(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(Float),
) -> TypeBuilder(a)
pub fn non_null_list_int(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(Int),
) -> TypeBuilder(a)
pub fn non_null_list_string(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> List(String),
) -> TypeBuilder(a)
pub fn non_null_string(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)

Add a non-null string field

pub fn nullable(
  inner: decode.Decoder(a),
) -> decode.Decoder(option.Option(a))

A decoder that returns None when the value is nil/null and otherwise runs the inner decoder.

Example

use email <- decode.optional_field("email", None, types.nullable(decode.string))
pub fn object(name: String) -> TypeBuilder(a)

Create a new type builder

pub fn object_field(
  builder: TypeBuilder(a),
  name: String,
  type_name: String,
  extractor: fn(a) -> dynamic.Dynamic,
) -> TypeBuilder(a)

Add a related object field

pub fn option(opt: option.Option(a)) -> dynamic.Dynamic

Convert an Option to Dynamic (None becomes Nil/null)

Example

#("age", types.option(user.age))
pub fn optional_bool(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(Bool),
) -> TypeBuilder(a)

Add an optional bool field. None becomes JSON null; Some(v) becomes the bool value.

pub fn optional_date(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)
pub fn optional_datetime(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)
pub fn optional_email(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)
pub fn optional_float(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(Float),
) -> TypeBuilder(a)

Add an optional float field. None becomes JSON null; Some(v) becomes the float value.

pub fn optional_id(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)

Add a nullable ID field

pub fn optional_int(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(Int),
) -> TypeBuilder(a)

Add an optional int field. None becomes JSON null; Some(v) becomes the int value.

pub fn optional_json(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(dynamic.Dynamic),
) -> TypeBuilder(a)
pub fn optional_string(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)

Add an optional string field. None becomes JSON null; Some(v) becomes the string value. Use this for nullable GraphQL fields.

pub fn optional_url(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)
pub fn optional_uuid(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> option.Option(String),
) -> TypeBuilder(a)
pub fn record(
  fields: List(#(String, dynamic.Dynamic)),
) -> dynamic.Dynamic

Build a Dynamic dict from a list of field tuples

This is a convenience helper for creating DataLoader encoders.

Example

fn user_to_dynamic(u: User) -> Dynamic {
  types.record([
    #("id", types.to_dynamic(u.id)),
    #("name", types.to_dynamic(u.name)),
    #("email", types.to_dynamic(u.email)),
  ])
}
pub fn string(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)

Add a string field

pub fn to_dynamic(value: a) -> dynamic.Dynamic

Convert any Gleam value to Dynamic This uses unsafe_coerce under the hood

pub fn url(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)
pub fn uuid(
  builder: TypeBuilder(a),
  name: String,
  extractor: fn(a) -> String,
) -> TypeBuilder(a)
pub fn value(builder: EnumBuilder, name: String) -> EnumBuilder

Add an enum value

pub fn value_with_desc(
  builder: EnumBuilder,
  name: String,
  desc: String,
) -> EnumBuilder

Add an enum value with description

Search Document