Skip to content

Schemas

A schema pairs a Gale type with the rules for crossing a dynamic boundary. The compiler checks that a value and its schema agree on the type; the schema checks incoming data and value constraints at runtime.

Profile is a nominal struct with a JSON schema in schema_example.

module schema_example.profile
@moduledoc """
`Profile` is a nominal struct with a JSON schema in `schema_example`.
"""
struct {
name: Binary,
age: Integer,
email: Option<Binary>,
tags: List<Binary>
}

Event is a tagged union. Its JSON representation carries the tag in a "type" field; the schema dispatches on that field through one_of.

module schema_example.event
@moduledoc """
`Event` is a tagged union. Its JSON representation carries the tag in a
`"type"` field; the schema dispatches on that field through `one_of`.
"""
pub type Event = Started(name: Binary) | Stopped(reason: Binary)

Schemas for the schema_example.profile.Profile struct and the schema_example.event.Event tagged union.

A schema is one explicit value that validates unknown terms, decodes JSON, and encodes JSON, checking the same refinements in every direction. Passing profile_schema() next to a Profile is compile-time evidence that the pair agree on the type; no derivation or trait lookup is involved.

module schema_example
@moduledoc """
Schemas for the `schema_example.profile.Profile` struct and the `schema_example.event.Event` tagged union.
A schema is one explicit value that validates unknown terms, decodes JSON,
and encodes JSON, checking the same refinements in every direction. Passing
`profile_schema()` next to a `Profile` is compile-time evidence that the
pair agree on the type; no derivation or trait lookup is involved.
"""
alias gale_schema as schema
alias gale_schema.int as int
alias gale_schema.text as text
@doc """
A `Profile` schema with a refined field, an optional field, and a defaulted
field.
"""
pub fn profile_schema() -> schema.JsonSchema<schema_example.profile.Profile> {
schema.object4(
fn(name: Binary, age: Integer, email: Option<Binary>, tags: List<Binary>) =>
schema_example.profile.Profile {
name: name,
age: age,
email: email,
tags: tags
},
schema.field(
"name",
schema.string() |> text.min_length(1),
fn(value: schema_example.profile.Profile) => value.name
),
schema.field(
"age",
schema.integer() |> int.at_least(0),
fn(value: schema_example.profile.Profile) => value.age
),
schema.optional(
"email",
schema.string(),
fn(value: schema_example.profile.Profile) => value.email
),
schema.defaulted(
"tags",
schema.list(schema.string()),
[],
fn(value: schema_example.profile.Profile) => value.tags
)
)
}
@doc """
Parses a profile from a JSON binary.
"""
pub fn decode_profile(
source: Binary
) -> Result<schema_example.profile.Profile, gale_schema.JsonError> {
schema.parse(profile_schema(), source)
}
@doc """
Encodes a profile to a JSON binary, refusing values that fail refinement.
"""
pub fn encode_profile(
value: schema_example.profile.Profile
) -> Result<Binary, List<gale_schema.Issue>> {
schema.stringify(profile_schema(), value)
}
fn started_schema() -> schema.JsonSchema<schema_example.event.Event> {
schema.object2(
fn(kind: Binary, name: Binary) => schema_example.event.Event.Started(name: name),
schema.field(
"type",
schema.enumeration(["started"]),
fn(value: schema_example.event.Event) => event_kind(value)
),
schema.field("name", schema.string(), fn(value: schema_example.event.Event) => started_name(value))
)
}
fn stopped_schema() -> schema.JsonSchema<schema_example.event.Event> {
schema.object2(
fn(kind: Binary, reason: Binary) => schema_example.event.Event.Stopped(reason: reason),
schema.field(
"type",
schema.enumeration(["stopped"]),
fn(value: schema_example.event.Event) => event_kind(value)
),
schema.field(
"reason",
schema.string(),
fn(value: schema_example.event.Event) => stopped_reason(value)
)
)
}
fn event_kind(value: schema_example.event.Event) -> Binary {
match value {
schema_example.event.Event.Started(_) -> "started"
schema_example.event.Event.Stopped(_) -> "stopped"
}
}
fn started_name(value: schema_example.event.Event) -> Binary {
match value {
schema_example.event.Event.Started(name) -> name
schema_example.event.Event.Stopped(_) -> ""
}
}
fn stopped_reason(value: schema_example.event.Event) -> Binary {
match value {
schema_example.event.Event.Started(_) -> ""
schema_example.event.Event.Stopped(reason) -> reason
}
}
@doc """
An `Event` schema: decoding tries each variant, and encoding dispatches on
the constructor with an explicit match.
"""
pub fn event_schema() -> schema.JsonSchema<schema_example.event.Event> {
schema.of_json(
fn(json) =>
schema.decode(schema.one_of([started_schema(), stopped_schema()]), json),
fn(value: schema_example.event.Event) => match value {
schema_example.event.Event.Started(name) ->
gale_std.json.object([
("type", gale_std.json.string("started")),
("name", gale_std.json.string(name))
])
schema_example.event.Event.Stopped(reason) ->
gale_std.json.object([
("type", gale_std.json.string("stopped")),
("reason", gale_std.json.string(reason))
])
}
)
}
@doc """
Parses an event from a JSON binary.
"""
pub fn decode_event(source: Binary) -> Result<schema_example.event.Event, gale_schema.JsonError> {
schema.parse(event_schema(), source)
}
@doc """
Encodes an event to a JSON binary.
"""
pub fn encode_event(value: schema_example.event.Event) -> Result<Binary, List<gale_schema.Issue>> {
schema.stringify(event_schema(), value)
}

These three modules form one project. The source blocks above are checked for exact agreement with the example; its Mix tests check them together, including successful round trips and rejection of invalid fields.