glon

A Gleam library for JSON Schema generation and decoding. Define a schema once, then use it to both generate a JSON Schema string and decode JSON values into typed Gleam data.

Package Version Hex Docs

gleam add glon@1

How it works

JsonSchema(t) is an opaque type that pairs a JSON Schema definition with a decoder. When you build a schema using the builder API, you get a value that can:

The schema and decoder are always in sync – if you say a field is a string, the decoder knows to decode a string.

Quick start

import glon

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

fn user_schema() {
  use name <- glon.field("name", glon.string())
  use age <- glon.field("age", glon.integer())
  glon.success(User(name:, age:))
}

pub fn main() {
  let schema = user_schema()

  // Generate JSON Schema
  glon.to_string(schema)
  // -> {"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"}},"required":["name","age"]}

  // Decode JSON values
  glon.decode(schema, from: "{\"name\":\"Alice\",\"age\":30}")
  // -> Ok(User(name: "Alice", age: 30))
}

Full example

A more realistic schema with nested objects, arrays, optional fields, nullable fields, and descriptions:

import gleam/option.{type Option}
import glon

pub type Address {
  Address(street: String, city: String, zip: Option(String))
}

pub type Tag {
  Tag(key: String, value: String)
}

pub type Company {
  Company(
    name: String,
    founded_year: Int,
    public: Bool,
    rating: Option(Float),
    address: Address,
    tags: List(Tag),
    website: Option(String),
    phone: Option(String),
  )
}

fn address_schema() {
  use street <- glon.field("street", glon.string() |> glon.describe("Street address"))
  use city <- glon.field("city", glon.string())
  use zip <- glon.optional("zip", glon.string() |> glon.describe("ZIP or postal code"))
  glon.success(Address(street:, city:, zip:))
}

fn tag_schema() {
  use key <- glon.field("key", glon.string())
  use value <- glon.field("value", glon.string())
  glon.success(Tag(key:, value:))
}

fn company_schema() {
  use name <- glon.field("name", glon.string() |> glon.describe("Legal company name"))
  use founded_year <- glon.field("founded_year", glon.integer() |> glon.describe("Year the company was founded"))
  use public <- glon.field("public", glon.boolean() |> glon.describe("Whether publicly traded"))
  use rating <- glon.optional_or_null("rating", glon.number() |> glon.describe("Rating from 0.0 to 5.0"))
  use address <- glon.field("address", address_schema())
  use tags <- glon.field("tags", glon.array(of: tag_schema()) |> glon.describe("Categorization tags"))
  use website <- glon.optional("website", glon.string())
  use phone <- glon.optional_or_null("phone", glon.string())
  glon.success(Company(name:, founded_year:, public:, rating:, address:, tags:, website:, phone:))
}

glon.to_string(company_schema()) produces:

{
  "type": "object",
  "properties": {
    "name": { "type": "string", "description": "Legal company name" },
    "founded_year": { "type": "integer", "description": "Year the company was founded" },
    "public": { "type": "boolean", "description": "Whether publicly traded" },
    "rating": { "type": ["number", "null"], "description": "Rating from 0.0 to 5.0" },
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string", "description": "Street address" },
        "city": { "type": "string" },
        "zip": { "type": "string", "description": "ZIP or postal code" }
      },
      "required": ["street", "city"]
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "key": { "type": "string" },
          "value": { "type": "string" }
        },
        "required": ["key", "value"]
      },
      "description": "Categorization tags"
    },
    "website": { "type": "string" },
    "phone": { "type": ["string", "null"] }
  },
  "required": ["name", "founded_year", "public", "address", "tags"]
}

The same schema decodes JSON into typed Gleam values:

// All fields present
glon.decode(company_schema(), from: "{\"name\":\"Acme Corp\",\"founded_year\":1995,\"public\":true,\"rating\":4.5,\"address\":{\"street\":\"123 Main St\",\"city\":\"Springfield\",\"zip\":\"62704\"},\"tags\":[{\"key\":\"industry\",\"value\":\"tech\"}],\"website\":\"https://acme.example.com\",\"phone\":\"+1-555-0100\"}")
// -> Ok(Company(name: "Acme Corp", founded_year: 1995, public: True, rating: Some(4.5), ...))

// Only required fields
glon.decode(company_schema(), from: "{\"name\":\"Tiny LLC\",\"founded_year\":2020,\"public\":false,\"address\":{\"street\":\"1 Elm St\",\"city\":\"Shelbyville\"},\"tags\":[]}")
// -> Ok(Company(name: "Tiny LLC", ..., rating: None, website: None, phone: None))

// Explicit nulls
glon.decode(company_schema(), from: "{\"name\":\"Null Inc\",\"founded_year\":2010,\"public\":true,\"rating\":null,\"address\":{\"street\":\"0 Zero Rd\",\"city\":\"Nowhere\"},\"tags\":[],\"phone\":null}")
// -> Ok(Company(name: "Null Inc", ..., rating: None, phone: None))

API reference

Primitives

FunctionTypeJSON Schema
glon.string()JsonSchema(String){"type": "string"}
glon.integer()JsonSchema(Int){"type": "integer"}
glon.number()JsonSchema(Float){"type": "number"}
glon.boolean()JsonSchema(Bool){"type": "boolean"}

Composites

FunctionTypeJSON Schema
glon.array(of: schema)JsonSchema(List(t)){"type": "array", "items": ...}
glon.nullable(schema)JsonSchema(Option(t)){"type": ["<t>", "null"]}

Object fields

FunctionRequired?Nullable?Gleam type
glon.fieldyesnot
glon.optionalnonoOption(t)
glon.optional_or_nullnoyesOption(t)
glon.field_with_defaultnonot (uses default when absent)

All four are used with Gleam’s use syntax to chain fields:

use value <- glon.field("name", glon.string())
use value <- glon.optional("name", glon.string())
use value <- glon.optional_or_null("name", glon.string())
use value <- glon.field_with_default("port", glon.integer(), default: 8080, encode: json.int)

Enum / Const

FunctionTypeJSON Schema
glon.enum(["a", "b"])JsonSchema(String){"type": "string", "enum": ["a", "b"]}
glon.enum_map([#("a", A), #("b", B)])JsonSchema(t){"type": "string", "enum": ["a", "b"]}
glon.constant("a")JsonSchema(String){"type": "string", "const": "a"}
glon.constant_map("a", A)JsonSchema(t){"type": "string", "const": "a"}

The _map variants decode to a custom Gleam type instead of String:

type Color { Red Green Blue }

// Decodes to String
glon.enum(["red", "green", "blue"])

// Decodes to Color
glon.enum_map([#("red", Red), #("green", Green), #("blue", Blue)])

Combinators

FunctionJSON SchemaDescription
glon.map(schema, transform)(unchanged)Transform decoded type without changing schema
glon.one_of([a, b, ...]){"oneOf": [...]}Value must match exactly one sub-schema
glon.any_of([a, b, ...]){"anyOf": [...]}Value must match at least one sub-schema
glon.tagged_union("type", [...]){"oneOf": [...]} with discriminatorDiscriminated union with tag field

Use map to align types for one_of / any_of:

type Value { TextVal(String) NumVal(Int) }

let schema = glon.one_of([
  glon.string() |> glon.map(TextVal),
  glon.integer() |> glon.map(NumVal),
])

Use tagged_union for discriminated unions:

type Shape { Circle(Float) Square(Float) }

let schema = glon.tagged_union("type", [
  #("circle", {
    use radius <- glon.field("radius", glon.number())
    glon.success(Circle(radius))
  }),
  #("square", {
    use side <- glon.field("side", glon.number())
    glon.success(Square(side))
  }),
])

String validation

All string constraints are enforced during decode.

FunctionJSON SchemaDecode behavior
glon.min_length(schema, n){"minLength": n}Rejects strings shorter than n
glon.max_length(schema, n){"maxLength": n}Rejects strings longer than n
glon.pattern(schema, regex){"pattern": "..."}Rejects strings not matching the regex
glon.string()
|> glon.min_length(1)
|> glon.max_length(100)
|> glon.pattern("^[a-zA-Z]+$")

Number validation

All number constraints are enforced during decode. Constraint values are Float, and work on both integer() and number() schemas.

FunctionJSON SchemaDecode behavior
glon.minimum(schema, n){"minimum": n}Rejects values < n
glon.maximum(schema, n){"maximum": n}Rejects values > n
glon.exclusive_minimum(schema, n){"exclusiveMinimum": n}Rejects values <= n
glon.exclusive_maximum(schema, n){"exclusiveMaximum": n}Rejects values >= n
glon.multiple_of(schema, n){"multipleOf": n}Rejects values not a multiple of n
glon.integer()
|> glon.minimum(0.0)
|> glon.maximum(100.0)

glon.number()
|> glon.exclusive_minimum(0.0)
|> glon.multiple_of(0.5)

Annotations

glon.string() |> glon.describe("A human-readable description")

Operations

glon.to_string(schema)                    // -> String (JSON Schema)
glon.to_json(schema)                      // -> json.Json (for embedding in larger structures)
glon.decode(schema, from: json_string)    // -> Result(t, json.DecodeError)

JSON Schema coverage

FeatureStatusNotes
Types
string✅ Supported
integer✅ Supported
number✅ Supported
boolean✅ Supported
array✅ Supported
object✅ SupportedNested objects, required/optional fields
null / nullable✅ SupportedVia nullable, optional_or_null
enum✅ SupportedString values via enum, enum_map
const✅ SupportedString values via constant, constant_map
Composition
oneOf✅ SupportedVia one_of, tagged_union
anyOf✅ SupportedVia any_of
allOf🚫 Out of scopeIncompatible with Gleam’s type system
not🔲 Not yetNegation
$ref / $defs🔲 Not yetReusable schema definitions
Object keywords
properties✅ Supported
required✅ Supported
additionalProperties🔲 Not yet
patternProperties🔲 Not yet
propertyNames🔲 Not yet
minProperties / maxProperties🔲 Not yet
dependentRequired / dependentSchemas🔲 Not yet
Array keywords
items✅ Supported
prefixItems🔲 Not yetTuple validation
minItems / maxItems🔲 Not yet
uniqueItems🔲 Not yet
contains🔲 Not yet
String validation
minLength / maxLength✅ SupportedVia min_length, max_length
pattern✅ SupportedVia pattern
format🚫 Out of scopeValidating formats is out of scope
Number validation
minimum / maximum✅ SupportedVia minimum, maximum
exclusiveMinimum / exclusiveMaximum✅ SupportedVia exclusive_minimum, exclusive_maximum
multipleOf✅ SupportedVia multiple_of
Annotations
description✅ SupportedVia describe
title🔲 Not yet
default✅ SupportedVia field_with_default
examples🔲 Not yet
deprecated🔲 Not yet
readOnly / writeOnly🔲 Not yet
Conditional
if / then / else🔲 Not yet
Meta
$schema🔲 Not yetDraft identifier
$id🔲 Not yet
$comment🔲 Not yet

Compatibility

Search Document