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
-
EnumBuilder( name: String, description: option.Option(String), values: List(EnumValue), )
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
-
EnumValue( name: String, description: option.Option(String), is_deprecated: Bool, deprecation_reason: option.Option(String), )
Builder for GraphQL input object types
pub type InputBuilder {
InputBuilder(
name: String,
description: option.Option(String),
fields: List(InputField),
)
}
Constructors
-
InputBuilder( name: String, description: option.Option(String), fields: List(InputField), )
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
-
InputField( name: String, description: option.Option(String), field_type: schema.FieldType, default_value: option.Option(dynamic.Dynamic), )
Builder for creating GraphQL object types
pub type TypeBuilder(a) {
TypeBuilder(
name: String,
description: option.Option(String),
fields: List(TypeField(a)),
)
}
Constructors
-
TypeBuilder( name: String, description: option.Option(String), fields: List(TypeField(a)), )
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
-
TypeField( name: String, description: option.Option(String), field_type: schema.FieldType, extractor: fn(a) -> dynamic.Dynamic, is_deprecated: Bool, deprecation_reason: option.Option(String), )Simple field with extractor
-
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), )Field with arguments and resolver
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_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 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_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_with_desc(
builder: EnumBuilder,
name: String,
desc: String,
) -> EnumBuilder
Add an enum value with description