moon_zod

A runtime JSON schema validation library for MoonBit, inspired by Zod and Pydantic

json
schema
validation
zod
llm
tool-calling
moon add Betterlol/moon_zod@0.8.2
Download zip
Author
Version
0.8.2
License
Apache-2.0
Last updated
last month
Downloads
64

Dependencies

README

#moon_zod

CI Mooncakes doc


#Documents

DocumentDescription
Design DocumentImportant! Core architecture, design decisions, and future directions
API ReferenceDetailed API documentation
CLI ReferenceCommand-line usage
BenchmarkPerformance comparison with other validation libraries
ExamplesPractical usage examples
Exportersmoon_zod exporters documentation
Importersmoon_zod importers documentation


#About

moon_zod is a runtime Schema intermediate representation (IR) — a validation contract layer decoupled from input sources and output targets. It provides runtime JSON schema validation with a fluent chainable API, designed primarily for LLM Tool Calling, and serves as a cross-format schema interoperability bridge. See DESIGN.md for details.

A Schema IR core providing runtime validation, multi-source import, multi-format export, and LLM hallucination defense — closing the loop from JSON Schema to prompt generation.


#Installation

moon add Betterlol/moon_zod

Or add to moon.mod:

import { "Betterlol/moon_zod", }


#Quick Start

let schema = @moon_zod.object({
"name": @moon_zod.string().min(2).max(50),
"age": @moon_zod.number().int().min(0).max(150),
"email": @moon_zod.string().email(),
})

match schema.parse(input_json) {
Ok(valid) => {
println("Valid")
println(@debug.to_string(valid))
}
Err(errors) => {
println("Invalid")
println(errors.length().to_string())
for e in errors {
println(e.to_string())
}
}
}

Zero-code CLI validation:
# Infer schema from sample, validate data moon run cmd/validate -- '{"name":"Alice","age":30}' '{"name":"Bob","age":25}' # PASS # Batch validation with JSON Lines moon run cmd/validate -- '{"name":"Alice"}' '{"name":"Bob"}\n{"name":"Eve"}' # Results: 2 passed, 0 failed


#✨ Why MoonZod? (AI-First)

Featuremoon_zodTypical Validation
Error collectionCollects all errors in one passMost libs fail-fast on first error
Hallucination defenseDefault Strip mode silently removes unknown fieldsWould pass through hallucinated data
Named schema exportschema_to_prompt_named() generates modular TypeScript interfaces with type name referencesInline expansion + duplication
JSON Schema exportto_json_schema() generates standard schema for LLM APIManual schema maintenance
Path precisionEvery error includes exact field path (users[0].profile.age)Often just a flat message
Wasm-readyMutable path stack — zero heap allocation on success pathString-heavy allocation per parse

In LLM Tool Calling, the model often produces multiple errors at once and hallucinates extra fields. moon_zod collects every error in a single pass (so you can send them all back for self-correction), and strips unknown fields by default (no silent data corruption from hallucinated keys).


#Project Layout

moon_zod/ ā”œā”€ā”€ core/ # Core schema validation library │ ā”œā”€ā”€ types.mbt # ValidationError, SchemaResult, core types │ ā”œā”€ā”€ schema.mbt # Schema struct, parse dispatch, path stack │ ā”œā”€ā”€ string.mbt # string() factory + validators (trim, to_lower, to_upper) │ ā”œā”€ā”€ number.mbt # number() factory + validators │ ā”œā”€ā”€ boolean.mbt # boolean() factory │ ā”œā”€ā”€ null.mbt # null() factory │ ā”œā”€ā”€ bigint.mbt # bigint() factory │ ā”œā”€ā”€ any_unknown.mbt # any() / unknown() pass-through schemas │ ā”œā”€ā”€ array.mbt # array() factory + parse_array │ ā”œā”€ā”€ tuple.mbt # tuple() factory + parse_tuple │ ā”œā”€ā”€ object.mbt # object() + modes (strip/passthrough/strict), pick/omit/partial/extend/merge │ ā”œā”€ā”€ optional.mbt # optional() factory │ ā”œā”€ā”€ default.mbt # default() factory │ ā”œā”€ā”€ enum.mbt # enum_values() factory │ ā”œā”€ā”€ literal.mbt # literal() factory (constant values) │ ā”œā”€ā”€ union.mbt # union() factory │ ā”œā”€ā”€ intersection.mbt # intersection() / intersect() │ ā”œā”€ā”€ refine.mbt # refine() for custom validation │ ā”œā”€ā”€ transform.mbt # transform() for data transformation │ ā”œā”€ā”€ preprocess.mbt # preprocess() for input preprocessing │ ā”œā”€ā”€ shared_utils.mbt # Common utilities (unwrap_schema, peel_optional, etc.) │ ā”œā”€ā”€ constraint_extractor.mbt # Extract constraint info from rules │ └── moon_zod_wbtest.mbt # White-box tests (path stack invariants) │ ā”œā”€ā”€ combinators/ # Schema combinator utilities │ └── schema_combinators.mbt # Schema composition helpers │ ā”œā”€ā”€ exporters/ # Code/schema export tools │ ā”œā”€ā”€ prompt.mbt # schema_to_prompt() / schema_to_prompt_named() │ ā”œā”€ā”€ prompt_renderer.mbt # Trait-based prompt rendering │ ā”œā”€ā”€ json_schema.mbt # to_json_schema() / to_json_schema_named() │ ā”œā”€ā”€ json_schema_renderer.mbt # Trait-based JSON Schema rendering │ ā”œā”€ā”€ moonbit_struct.mbt # schema_to_moonbit_struct() + static to_schema() generation │ └── schema_exporter.mbt # Shared exporter utilities │ ā”œā”€ā”€ importers/ # Schema import tools │ └── from_json_schema.mbt # json_schema_to_moon_zod() — reverse JSON Schema → moon_zod code generation │ ā”œā”€ā”€ tests/ # Test suite (466 tests) │ ā”œā”€ā”€ test_string.mbt # string() validator tests (trim, to_lower, to_upper, nonempty) │ ā”œā”€ā”€ test_number.mbt # number() validator tests │ ā”œā”€ā”€ test_boolean_null.mbt # boolean/null tests │ ā”œā”€ā”€ test_object.mbt # object() mode + pick/omit/partial/extend/merge tests │ ā”œā”€ā”€ test_array.mbt # array() + nonempty tests │ ā”œā”€ā”€ test_tuple.mbt # tuple() tests │ ā”œā”€ā”€ test_combinators.mbt # union/literal/optional/default/brand/bigint tests │ ā”œā”€ā”€ test_any_unknown_preprocess.mbt # any/unknown/preprocess tests │ ā”œā”€ā”€ test_transform_refine.mbt # transform/refine tests │ ā”œā”€ā”€ test_json_schema.mbt # JSON Schema export + $defs/$ref tests │ ā”œā”€ā”€ test_json_schema_fixes.mbt # exclusiveMin/Max semantics + enum edge cases │ ā”œā”€ā”€ test_moonbit_struct.mbt # MoonBit struct generation tests │ ā”œā”€ā”€ test_prompt.mbt # Prompt generation tests │ ā”œā”€ā”€ test_prompt_named.mbt # Named schema export tests │ ā”œā”€ā”€ test_custom_message.mbt # Custom error message tests │ ā”œā”€ā”€ test_errors.mbt # Error collection tests │ ā”œā”€ā”€ test_schema_to_code.mbt # Code generation tests │ └── reexporter.mbt # Test re-exports │ ā”œā”€ā”€ cmd/ # CLI tools │ ā”œā”€ā”€ main/ # Benchmark runner (performance baselines) │ ā”œā”€ā”€ wasm/ # WebAssembly cross-language benchmark │ ā”œā”€ā”€ json2schema/ # JSON → moon_zod schema code generator + JSON Schema reverse importer │ ā”œā”€ā”€ gen-struct/ # JSON Schema → MoonBit struct generator │ └── validate/ # JSON schema validator (infer-then-validate) │ └── examples/ # LLM agent demonstrations ā”œā”€ā”€ gen-struct/ # JSON Schema → MoonBit struct generator demo ā”œā”€ā”€ json2schema/ # JSON → moon_zod schema code generation ā”œā”€ā”€ mock/ # Mock agent demonstrations │ ā”œā”€ā”€ llm_agent/ # Basic LLM tool calling example │ └── educational_agent/ # Multi-round self-correction demo ā”œā”€ā”€ multiple_schemas/ # Handling multiple schemas ā”œā”€ā”€ real_llm_agent/ # Real LLM integration (with API fallback to mock) ā”œā”€ā”€ resources/ # Sample data files (JSON, JSON Schema) ā”œā”€ā”€ schema2json/ # Schema → JSON Schema export demo ā”œā”€ā”€ schema2prompt/ # Schema → prompt generation showcase ā”œā”€ā”€ shared_schemas/ # Shared schema definitions (library package) └── validate_cli/ # CLI validation demo


#Development

# Testing & Building moon test # Run all tests (466 total, 0 warnings) moon build # Build the library moon check # Type check (0 errors, 0 warnings) moon info && moon fmt # Update interface + format # CLI Tools moon run cmd/main # Run performance benchmarks moon run cmd/json2schema -- '{"hello":"world"}' # JSON → moon_zod schema code moon run cmd/json2schema -- --from-json-schema '<{...}>' # JSON Schema → moon_zod code moon run cmd/json2schema -- --from-json-schema '<{...}>' --verbose # with debug output moon run cmd/gen-struct -- --schema '<{...}>' # JSON Schema → MoonBit structs moon run cmd/validate -- '{"name":"Alice"}' '{"name":"Bob"}' # Validate JSON # Examples moon run examples/mock/llm_agent # Basic LLM tool calling demo moon run examples/mock/educational_agent # Multi-round self-correction demo moon run examples/real_llm_agent -- product prompt # Real LLM with mock fallback moon run examples/real_llm_agent -- product validate # Validate with real API moon run examples/multiple_schemas # Multiple schema handling moon run examples/schema2json -- product schema # Schema → JSON Schema export moon run examples/schema2prompt -- product schema # Schema → prompt generation showcase moon run examples/json2schema # JSON → moon_zod schema code gen


#Features

  • Primitive schemas: string(), number(), boolean(), null(), bigint()
  • Compound schemas: object(Map), array(Schema), tuple([Schema...]), union(Array[Schema]), intersection(Array[Schema]), enum_values(Array[String]), literal(Json)
  • Pass-through schemas: any() and unknown() accept any JSON value (semantic distinction)
  • String validators (23+): .min(n), .max(n), .nonempty(), .trim(), .to_lower(), .to_upper(), .email() (full RFC validation), .url() (full structure), .regex(pattern) (regular expression match), .startsWith(), .endsWith(), .includes(), .uuid(), .cuid(), .ulid(), .datetime(), .ip()/.ipv4()/.ipv6(), .length(n)
  • Number validators (8+): .int(), .positive(), .negative(), .multipleOf(), .finite(), .safe(), .min(), .max()
  • Object modes: .strip() (default, removes unknown fields), .passthrough() (keeps unknown fields), .strict() (rejects unknown fields)
  • Object composition: .pick(keys), .omit(keys), .partial(), .extend_with(Map), .merge(Schema)
  • Optional/Default handling: .optional() and .default(value) with correct rule chaining through wrappers
  • Data transformation: .transform(fn) validates then transforms; preprocess(fn, schema) transforms then validates
  • Custom rules: .refine(check, message), .intersect(other) for explicit intersection
  • Schema naming & metadata: .name(text) for named exports, .describe(text) for LLM prompts, .brand(text) for nominal typing
  • Custom error messages: msg? parameter on all validators, .message(text) override method, type-level required_error / invalid_type_error
  • Error collection: Collects all validation errors in one pass, perfect for LLM self-correction loops
  • Full-path error reporting: Every error includes exact field path (users[0].profile.age)
  • LLM prompt generation:
    • schema_to_prompt(schema) — inline TypeScript-interface with constraint comments
    • schema_to_prompt_named(schema, include_names?) — modular interfaces with topological sorting and type name references
  • JSON Schema export:
    • to_json_schema(schema) — standard JSON Schema with full constraint annotations
    • to_json_schema_skeleton(schema) — lightweight skeleton (structure only)
    • to_json_schema_named(schema, include_names?) — separate $defs and $ref references
  • JSON Schema reverse import:
    • json_schema_to_moon_zod(json_schema) — generate moon_zod source code from standard JSON Schema
    • Full support for $defs, $ref, constraints, format validation, enum
  • MoonBit struct generation:
    • schema_to_moonbit_struct(schema) — recursively generate MoonBit struct/enum definitions for every object/enum schema
    • schema_to_moonbit_struct_full(schema) — generate definitions plus static Type::to_schema() functions
  • Lightweight dependencies: Core MoonBit library plus official moonbitlang/regexp for regex validation
  • WebAssembly-ready: Mutable path stack for zero heap allocation on success path
  • Performance: ~18.5k-56k validations/second depending on schema complexity


website: extension.


#Learn More

#
ConstraintInfo

Re-export core types, factory functions, and utilities.

#
JsonSchemaRenderer

Trait for rendering @core.Schema as a Json value (JSON @core.Schema export).

Each method corresponds to one @core.SchemaType variant and returns a Json value representing the JSON @core.Schema constraint.

#
ObjectMode

Re-export core types, factory functions, and utilities.

#
Rule

Re-export core types, factory functions, and utilities.

#
Schema

Re-export core types, factory functions, and utilities.

#
SchemaResult

Result type returned by Schema::parse. Ok(json) on success, Err(errors) with all collected errors on failure.

#
SchemaType

Re-export core types, factory functions, and utilities.

#
StringRenderer

Trait for rendering @core.Schema as a String (used by prompt.mbt and moonbit_struct.mbt).

Each method corresponds to one @core.SchemaType variant. The indent parameter controls nesting level for formatted output.

#
TransformClosure

Re-export core types, factory functions, and utilities.

#
ValidationError

Re-export core types, factory functions, and utilities.

#
any

fn any(required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that accepts any JSON value.

#
append_rule

fn append_rule(schema :
Schema
, check : (Json) -> Bool, message : String) ->
Schema

Append a rule through decoration wrappers.

#
append_rule_with_annotation

fn append_rule_with_annotation(schema :
Schema
, check : (Json) -> Bool, message : String, annotation : Json) ->
Schema

Append a rule with a JSON Schema annotation fragment. The annotation is merged into the JSON Schema output when to_json_schema() is called, enabling constraint export (minLength, maximum, pattern, etc.).

#
array

fn array(element_schema :
Schema
, required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates JSON arrays.

Each element in the array is validated against element_schema.

#
bigint

fn bigint(required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates JSON integer values as big integers.

This is a convenience alias for number().int() that expresses semantic intent. Useful for financial amounts, large IDs, and other scenarios where the semantic type is "big integer" rather than "number that is an integer".

#
boolean

fn boolean(required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates JSON booleans.

#
collect_named_schemas

Collect all named schemas from the input schema tree.

#
constraint_comment

fn constraint_comment(schema :
Schema
) -> String

Extract and format constraint comment for a schema (without description). Dispatches to type-appropriate formatter based on the unwrapped schema type.

#
constraint_info_default

Create a default empty ConstraintInfo

#
constraint_info_to_array_comment

fn constraint_info_to_array_comment(info :
ConstraintInfo
) -> String

Convert ConstraintInfo to prompt comment string for arrays.

#
constraint_info_to_fallback_comment

fn constraint_info_to_fallback_comment(info :
ConstraintInfo
) -> String

Convert ConstraintInfo to prompt comment string (fallback for unknown types).

#
constraint_info_to_number_comment

fn constraint_info_to_number_comment(info :
ConstraintInfo
) -> String

Convert ConstraintInfo to prompt comment string for numbers.

#
constraint_info_to_string_comment

fn constraint_info_to_string_comment(info :
ConstraintInfo
) -> String

Convert ConstraintInfo to prompt comment string for strings.

#
enum_values

fn enum_values(values : Array[String], required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that accepts one of a fixed set of string values.

#
escape_ident

fn escape_ident(name : String) -> String

escape a string to be a valid MoonBit identifier by replacing invalid characters with underscores.

#
escape_mbt_string

fn escape_mbt_string(s : String) -> String

Escape a string for use in MoonBit code, handling backslashes, quotes, newlines, and tabs.

#
escape_variable_name

fn escape_variable_name(name : String) -> String

escape a string to be a valid MoonBit variable name (lowercase first letter, valid identifier).

#
extract_constraints

Extract all constraint information from a Rule array. Unified extraction that works across all types.

#
filter_named_schemas

fn filter_named_schemas(all_named : Array[
Schema
], include_names : Array[String]?) -> Array[
Schema
]

Filter named schemas based on include_names optional parameter. If include_names is None, returns all schemas. If Some(names), returns only schemas whose names are in the list.

#
format_double_simple

fn format_double_simple(v : Double) -> String

Format a Double as a string, stripping ".0" for whole numbers.

#
format_path

fn format_path(stack : Array[String]) -> String

#
indent_str

fn indent_str(n : Int) -> String

Generate n * 2 spaces.

#
inner_type

Peel OptionalType / DefaultType wrappers to find the effective base type.

#
intersection

fn intersection(schemas : Array[
Schema
], required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that requires all given schemas to match (intersection / and). For objects, fields from all schemas are merged into one.

#
is_optional_schema

fn is_optional_schema(s :
Schema
) -> Bool

#
join_parts

fn join_parts(parts : Array[String]) -> String

Join constraint parts with ", ".

#
json_infer_schema

fn json_infer_schema(json : Json) ->
Schema

Generate a schema from a JSON value. Simple heuristics are used to infer types and constraints from the value.

#
json_schema_to_moon_zod

fn json_schema_to_moon_zod(schema : Json) -> String

JSON @core.Schema → moon_zod source code

#
json_schema_to_prompt

fn json_schema_to_prompt(schema : Json) -> String

JSON @core.Schema → prompt

#
json_schema_to_schema

fn json_schema_to_schema(schema : Json) ->
Schema

JSON @core.Schema → @core.Schema @core.object

Parses a JSON @core.Schema (draft-07) document into a runtime @core.Schema @core.object. Handles $defs, $ref, enum, type constraints, and all JSON @core.Schema keywords.

#
literal

fn literal(value : Json, required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that accepts a specific literal JSON value. The value must match exactly (String, Number, Boolean, or Null).

Example

let schema = @moon_zod.literal(Json::string("hello"))
schema.parse(Json::string("hello")) // Ok
schema.parse(Json::string("world")) // Err

#
null

fn null(required_error? : String, invalid_type_error? : String) ->
Schema

Wrapper: null() factory can't be re-exported via pub using (keyword conflict)

#
number

fn number(required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates JSON numbers.

#
object

fn object(spec : Map[String,
Schema
], required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates JSON objects.

Each key in spec defines a required field and its schema. By default, extra fields in the input JSON are silently stripped (Strip mode). Use .passthrough() to allow extra fields, or .strict() to reject them.

#
parse_json

fn parse_json(input : String) -> Json

#
peel_optional

Peel OptionalType / DefaultType wrappers to check optionality.

#
preprocess

fn preprocess(f : (Json) -> Result[Json, String], inner :
Schema
, required_error? : String, invalid_type_error? : String) ->
Schema

Preprocess a JSON value before validating it with inner.

This is the inverse order of .transform(): f runs on raw input first, then the returned JSON value is validated against inner.

#
schema_to_moon_zod_code

fn schema_to_moon_zod_code(schema :
Schema
) -> String

Generate best-effort moon_zod source code from a @core.Schema @core.object (inline expansion).

Example

let schema = @moon_zod.string().min(3).describe("username")

let code = schema_to_moon_zod_code(schema)
// "@moon_zod.string().min(3).describe(\"username\")"
If root schema has no name, it is assigned the name "Root".

#
schema_to_moon_zod_code_inline_with_refs

fn schema_to_moon_zod_code_inline_with_refs(schema :
Schema
, include_names : Array[String]?) -> String

Generate best-effort inline code with optional named schema reference substitution.

If include_names is None, renders all schemas inline. If include_names is Some(names), replaces references to named schemas with variable names.

For use with schema_to_moon_zod_code_named() which produces separate definitions.

#
schema_to_moon_zod_code_named

fn schema_to_moon_zod_code_named(schema :
Schema
, include_names? : Array[String]?) -> String

Generate best-effort separate named schema definitions and return as newline-separated list.

Example

let named_list = schema_to_moon_zod_code_named(root_schema, None) // Returns: // "User: @moon_zod.object({ ... }).name(\"User\")\n // Profile: @moon_zod.object({ ... }).name(\"Profile\")"
If root schema has no name, it is assigned the name "Root".

#
schema_to_moonbit_struct

fn schema_to_moonbit_struct(schema :
Schema
) -> String

Convert a moon_zod schema into MoonBit type definitions.

Every ObjectType in the schema tree is emitted as a standalone pub struct. Schema names are treated only as type-name hints; anonymous nested objects are named from their owner and field path.

#
schema_to_moonbit_struct_full

fn schema_to_moonbit_struct_full(schema :
Schema
) -> String

Convert a moon_zod schema into MoonBit type definitions plus static Type::to_schema() -> @moon_zod.Schema functions for each generated type.

#
schema_to_prompt

fn schema_to_prompt(schema :
Schema
) -> String

Public API: convert a schema to a TypeScript-interface-style prompt @core.string.

#
schema_to_prompt_named

fn schema_to_prompt_named(schema :
Schema
, include_names? : Array[String]?) -> String

Public API: convert a schema with named sub-schemas into a prompt with separate interface definitions and name-based references.

If include_names is None, exports all named schemas. If include_names is Some(names), exports only the specified schema names. If root schema has no name, it is assigned the name "Root".

#
string

fn string(required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates JSON strings.

#
sub_index

fn sub_index(path : String, i : Int) -> String

#
sub_path

fn sub_path(path : String, name : String) -> String

#
to_json_schema

fn to_json_schema(schema :
Schema
) -> Json

Convert a moon_zod @core.Schema into a standard JSON @core.Schema @core.object with full constraint annotations (minLength, maximum, pattern, format, etc.).

#
to_json_schema_named

fn to_json_schema_named(schema :
Schema
, include_names? : Array[String]?) -> Json

Export a JSON @core.Schema with named sub-schemas in a $defs section. If root schema has no name, it is assigned the name "Root".

#
to_json_schema_skeleton

fn to_json_schema_skeleton(schema :
Schema
) -> Json

Convert a moon_zod @core.Schema into a lightweight JSON @core.Schema skeleton. Only structural type information is exported.

#
topological_sort_schemas

Topologically sort named schemas so dependents come before dependees.

#
tuple

fn tuple(items : Array[
Schema
], required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that validates fixed-length JSON arrays.

Each item in items validates the array element at the same index.

#
union

fn union(schemas : Array[
Schema
], required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that accepts any of the given schemas (union / or). Schemas are tried in order; the first match succeeds.

#
unknown

fn unknown(required_error? : String, invalid_type_error? : String) ->
Schema

Create a schema that accepts any JSON value as unknown.

Runtime behavior matches any(); the distinction is semantic for consumers.

#
unwrap_schema

Peel OptionalType / DefaultType / TransformType wrappers to find the innermost schema that carries the actual rules.

#
value_in_array

fn value_in_array(value : String, arr : Array[String]) -> Bool

Check if a value is in an array of strings.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

Ā© 2026 mooncakes.io