MoonCheck

    A lightweight, reusable JSON / API parameter validator with a CLI, written in MoonBit.

    json
    validation
    schema
    cli
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    2

    Dependencies

    #MoonCheck

    CI

    A lightweight, reusable JSON / API parameter validator with a CLI, written in MoonBit.

    MoonCheck validates JSON data against a small, hand-written schema and reports all problems it finds as clear, human-readable errors. It is a general purpose developer tool: validate REST API request bodies, JSON config files, or AI agent tool-call arguments before you trust them.

    #Features

    • Small schema language with the kinds you actually use: string, number, int, bool, object, array
    • Constraints: required, enum, numeric min/max, minLength/maxLength for strings, minItems/maxItems for arrays, items for array element types, and arbitrarily nested objects
    • Structured errors: every error carries a JSON-pointer-like path ($.user.age, $.tags[0]), a machine-readable kind, and a human message such as expected Int, got String
    • Collects all errors instead of stopping at the first
    • Built for CI: validate many documents in one run, with a machine-readable JSON report (--report json) and stable exit codes
    • Schema linting: mooncheck check-schema schema.json checks the schema document itself, so a broken contract fails before any data is checked
    • Shell globs work as-is (mooncheck validate s.json configs/*.json); the CLI needs no glob support and no extra dependency
    • Schemas are described as plain JSON documents, so they work across languages and are easy to read; they can also be built directly in MoonBit
    • No runtime dependencies beyond the MoonBit standard library

    #Installation / Build

    Requires the MoonBit toolchain.

    moon check # type-check the library and all packages moon test # run the test suite moon run examples/demo # run the demo (works on any backend)

    On a fresh machine, refresh the package registry index first if dependency resolution fails (module was not found in the registry):

    moon update

    The library part is pure MoonBit and runs on any backend. The cmd/main CLI package targets native builds; building the native executable needs an MSVC toolchain on Windows or a C toolchain on Linux/macOS:

    # Windows (MSVC) or Linux/macOS moon build cmd/main --target native

    #Quick Start

    Validate data from a schema document and a JSON string in one call:

    match @MoonCheck.validate_strings(schema_text, data_text) {
    Ok(errors) if errors.is_empty() => println("valid")
    Ok(errors) =>
    for error in errors {
    println(@MoonCheck.to_string(error))
    }
    Err(reason) => println("could not validate: \{reason}")
    }

    Or parse a schema once and reuse it against parsed JSON values:

    let schema = try { @MoonCheck.parse_schema_string(schema_text) } catch {
    _ => panic("bad schema")
    }
    let data = try { @json.parse(data_text) } catch { _ => panic("bad json") }
    if @MoonCheck.is_valid(schema, data) { ... }

    For batch work, parse the schema once, validate every document, and render one report — the same path the CLI takes:

    let results = [
    @MoonCheck.FileResult::new("a.json", @MoonCheck.validate_text(schema, text_a)),
    @MoonCheck.FileResult::new("b.json", @MoonCheck.validate_text(schema, text_b)),
    ]
    let report = @MoonCheck.RunReport::new("schema.json", results)
    println(@MoonCheck.render(report, @MoonCheck.ReportFormat::text(), false))
    println(@MoonCheck.render(report, @MoonCheck.ReportFormat::json(), false))

    #Schema Example

    Schemas are JSON documents. This one describes a small user object:

    { "type": "object", "properties": { "name": { "type": "string", "required": true, "minLength": 1, "maxLength": 30 }, "age": { "type": "int", "required": true, "min": 0, "max": 150 }, "role": { "type": "string", "required": false, "enum": ["admin", "user"] }, "tags": { "type": "array", "required": false, "maxItems": 5, "items": { "type": "string" } } } }

    Supported type values: string, number, int, bool, object, array.

    Supported keys:

    KeyApplies toMeaning
    typeallThe schema kind (required)
    requiredpropertyProperty must be present (true)
    enumpropertyValue must equal one of the listed literals
    min/maxint, numberInclusive numeric bounds
    minLength/maxLengthstringCharacter count bounds
    minItems/maxItemsarrayElement count bounds
    itemsarraySchema applied to every element (required)
    propertiesobjectMap of property name → property schema

    Objects with type: "object" validate the declared properties; undeclared fields are ignored. Properties are optional unless required: true.

    #Validation Example

    Given the schema above and this data:

    { "name": "", "age": 200, "role": "owner", "tags": ["a", "b"] }

    validate_strings returns four errors, printed one per line:

    $.name: length must be at least 1, got 0 $.age: value must be at most 150, got 200 $.role: value must be one of ["admin","user"] $.tags: length must be at most 5, got 4

    $.tags[0] would be the path of an invalid array element, and $.user.email the path of a field inside a nested object.

    #CLI Usage

    Build the executable first (see Installation / Build), then:

    # one schema, one data file — valid: prints nothing, exits 0 mooncheck validate examples/cli/schema.json examples/cli/data_valid.json # one schema, one data file — invalid: prints the errors, exits 1 mooncheck validate examples/cli/schema.json examples/cli/data_invalid.json # -> $.action: value must be one of ["click","type","scroll"] # $.x: value must be at least 0, got -5 # $.y: value must be at most 1080, got 5000

    The part that is not just another validator is batch/CI mode: pass several documents (the shell expands the glob) and each gets its own result, with a summary at the end.

    mooncheck validate examples/cli/config.schema.json examples/cli/configs/*.json

    ok: service_a.json ok: service_b.json service_c_broken.json: $.host: length must be at least 1, got 0 service_c_broken.json: $.port: value must be at most 65535, got 70000 service_c_broken.json: $.timeout: expected Int, got String checked 3 document(s): 2 ok, 1 failed, 3 error(s)

    Add --report json for a machine-readable report that a CI job or a script can consume directly:

    { "schema": "examples/cli/config.schema.json", "ok": false, "summary": { "checked": 3, "failed": 1, "errors": 3 }, "documents": [ { "path": "service_a.json", "ok": true, "errors": [] }, { "path": "service_b.json", "ok": true, "errors": [] }, { "path": "service_c_broken.json", "ok": false, "errors": [ { "path": "$.host", "kind": "too_short", "message": "length must be at least 1, got 0" }, { "path": "$.port", "kind": "above_max", "message": "value must be at most 65535, got 70000" }, { "path": "$.timeout", "kind": "type_mismatch", "message": "expected Int, got String" } ] } ] }

    Other commands:

    mooncheck check-schema schema.json # lint a schema document on its own mooncheck --help # usage mooncheck --version # version mooncheck validate s.json d.json -q # only report failures

    Exit codes: 0 every document is valid, 1 at least one document is invalid, 2 usage, I/O or schema error.

    The executable is a thin I/O shell: argument parsing, batch orchestration and report rendering live in cli.mbt and report.mbt in the library, so they are covered by the test suite on every backend. Only file reading and the process exit status live in cmd/main.

    #Use Cases

    • REST API request validation — check username, age, email and friends on an incoming request before handing the payload to handlers.
    • Configuration file validation — a tool reads a JSON config; MoonCheck verifies required keys, types, and ranges before the tool trusts it.
    • AI Agent / LLM tool-call validation — a model emits a tool call such as { "action": "click", "x": 123, "y": 456 }; validate fields, types, and ranges before dispatching to the executor.

    The same core also serves: test-data sanity checks, mocked-API fixtures, and any place a JSON payload must be trusted before it is used.

    #Ecosystem and Scope

    MoonBit already has JSON validation libraries, and this project does not try to replace them. Where MoonCheck sits:

    ProjectFocusInterface
    Betterlol/moon_zodZod/Pydantic-style runtime schemas for LLM tool calling: many kinds, combinators, strip/strict modes, JSON Schema export, prompt and struct-code generationMoonBit code API; CLI that infers a schema from a sample
    mizchi/jsonschemaJSON Schema (subset) validation plus MoonBit code generationMoonBit code API
    YumeCross/schemaLightweight JSON Schema validationMoonBit code API
    MoonCheckA schema-document-driven validator for checks and automation: a small fixed schema language, all-error collection with precise paths, and a batch/CI CLIJSON schema documents + MoonBit library + CLI

    What this project deliberately does not do: full JSON Schema Draft compatibility ($ref, anyOf/oneOf, pattern, …), data transformation, schema-to-code generation, prompt generation, or builder-style schema combinators. Those are already covered by the projects above, and re-implementing them would add surface without adding value.

    What it does instead: schemas are plain JSON files that any language, tool or pipeline can read and share; the CLI validates many documents in one run and can emit either human-readable lines or a stable JSON report with documented exit codes — the shape a CI job needs. The library has no dependencies beyond the MoonBit standard library, and the implementation is small enough to read in one sitting.

    In short: for rich in-code schemas or LLM-oriented features, use moon_zod. For a schema file that your CI, scripts and other languages can share, that is what MoonCheck is for.

    #Testing

    moon test

    The suite covers, for every kind, the normal path and each failure path: valid strings / ints / bools / objects / arrays, required-field presence, type mismatches, min/max bounds, string lengths, enum membership, nested objects, and array element types (with paths such as $.tags[0]). Schema-document parsing (valid and malformed) and end-to-end validate_strings calls are also covered.

    Command line parsing, the batch flow and both report renderings are tested as well, which is why they live in the library rather than in the executable: 69 tests, all green on the default backend.

    #Project Structure

    MoonCheck/ ├── moon.mod # module metadata ├── moon.pkg # root library package (no dependencies) ├── schema.mbt # schema type model (Type, specs, props) ├── schema_json.mbt # parse schema documents from JSON ├── validate.mbt # validation engine, validate_strings/validate_text ├── error.mbt # structured ValidationError / ErrorKind ├── report.mbt # text and JSON reports ├── cli.mbt # command line parsing + usage text ├── MoonCheck_test.mbt # black-box tests (public API) ├── MoonCheck_wbtest.mbt # white-box tests (engine internals) ├── cli_wbtest.mbt # white-box tests (CLI parsing + end-to-end flow) ├── report_wbtest.mbt # white-box tests (report rendering) ├── cmd/ │ └── main/ # CLI shell: reads files, prints, sets exit status └── examples/ ├── demo/ # runnable demo (moon run examples/demo) └── cli/ # sample schemas + data (configs/ for batch runs)

    #License

    Apache-2.0

    SchemaError

    pub(all) suberror SchemaError {
    InvalidSchema(String)
    } derive(Eq,
    Debug
    )

    Raised when a schema document is malformed.

    ArrSpec

    pub struct ArrSpec {
    item : Type
    min_length : Int?
    max_length : Int?
    }

    Constraint set for arrays.

    CheckSchemaOptions

    pub struct CheckSchemaOptions {
    path : String
    } derive(Eq,
    Debug
    )

    Options of the check-schema subcommand.

    CliCommand

    pub enum CliCommand {
    Help
    Version
    Validate(ValidateOptions)
    CheckSchema(CheckSchemaOptions)
    Invalid(String)
    } derive(Eq,
    Debug
    )

    A parsed command line.

    ErrorKind

    pub enum ErrorKind {
    MissingRequired
    Type
    BelowMinimum
    AboveMaximum
    TooShort
    TooLong
    NotInEnum
    InvalidNumber
    InvalidJson
    } derive(Eq,
    Debug
    )

    Classification of a single validation problem.

    FileResult

    pub struct FileResult {
    name : String
    errors : Array[ValidationError]
    } derive(Eq,
    Debug
    )

    The validation outcome for one data document.

    FileResult::new

    fn FileResult::new(name : String, errors : Array[ValidationError]) -> FileResult

    Create a FileResult.

    NumSpec

    pub struct NumSpec {
    min : Double?
    max : Double?
    }

    Constraint set shared by Int and Num (number) values.

    ObjSpec

    pub struct ObjSpec {
    properties : Map[String, Prop]
    }

    Constraint set for objects.

    Prop

    pub struct Prop {
    typ : Type
    required : Bool
    enums : Array[Json]?
    }

    A single declared property of an object schema.

    ReportFormat

    pub enum ReportFormat {
    Text
    Json
    } derive(Eq,
    Debug
    )

    Output format of a validation report.

    ReportFormat::json

    The JSON report format.

    ReportFormat::text

    The text report format.

    A constructor function, so callers in other packages do not have to spell out the variant.

    RunReport

    pub struct RunReport {
    schema : String
    results : Array[FileResult]
    } derive(Eq,
    Debug
    )

    The result of validating one schema against one or more data documents.

    RunReport::failed

    fn RunReport::failed(report : RunReport) -> Int

    Number of documents that failed validation.

    RunReport::is_ok

    fn RunReport::is_ok(report : RunReport) -> Bool

    true when every document is valid.

    RunReport::new

    fn RunReport::new(schema : String, results : Array[FileResult]) -> RunReport

    Create a RunReport.

    RunReport::total_errors

    fn RunReport::total_errors(report : RunReport) -> Int

    Total number of errors across all documents.

    StrSpec

    pub struct StrSpec {
    min_length : Int?
    max_length : Int?
    }

    Constraint set for string values.

    Type

    pub enum Type {
    Str(StrSpec)
    Num(NumSpec)
    Int(NumSpec)
    Bool
    Obj(ObjSpec)
    Arr(ArrSpec)
    }

    The schema of a JSON value.

    ValidateOptions

    pub struct ValidateOptions {
    format : ReportFormat
    quiet : Bool
    schema : String
    data : Array[String]
    } derive(Eq,
    Debug
    )

    Options of the validate subcommand.

    ValidationError

    pub struct ValidationError {
    path : String
    kind : ErrorKind
    message : String
    } derive(Eq,
    Debug
    )

    A single validation error.

    error_kind_name

    fn error_kind_name(kind : ErrorKind) -> String

    Stable, machine-readable name of an error kind.

    These strings are part of the JSON report format, so they are kept short, lowercase and stable rather than being derived from the constructor names.

    is_valid

    fn is_valid(schema : Type, value : Json) -> Bool

    Convenience: true when validate finds no error.

    kind_name

    fn kind_name(typ : Type) -> String

    The human-facing name of a schema kind, used in error messages. For example Int becomes "Int".

    parse_args

    fn parse_args(argv : ArrayView[String]) -> CliCommand

    Parse a command line (without the program name).

    validate is optional, so mooncheck validate s.json d.json and mooncheck s.json d.json are equivalent. Data paths may be given in any number; shells expand globs, so mooncheck s.json cfg/*.json works without any glob support in the CLI itself.

    parse_schema

    fn parse_schema(json : Json) -> Type raise SchemaError

    Parse a schema Type from its JSON representation.

    parse_schema_string

    fn parse_schema_string(input : String) -> Type raise SchemaError

    Parse a schema Type from a JSON document string.

    parse_schema_text

    fn parse_schema_text(text : String) -> Result[Type, String]

    Parse a schema document, returning the reason as a string on failure.

    Batch and CLI callers parse the schema once and reuse it for every data document, so a broken schema is reported separately from data errors.

    render

    fn render(report : RunReport, format : ReportFormat, quiet : Bool) -> String

    Render the report in the requested format.

    quiet only affects the text format; the JSON format is already machine-oriented and always contains everything.

    render_json

    fn render_json(report : RunReport) -> String

    Render the report as a JSON document.

    Shape:
    { "schema": "schema.json", "ok": false, "summary": { "checked": 2, "failed": 1, "errors": 2 }, "documents": [ { "path": "a.json", "ok": true, "errors": [] }, { "path": "b.json", "ok": false, "errors": [ { "path": "$.age", "kind": "type_mismatch", "message": "..." } ] } ] }

    render_text

    fn render_text(report : RunReport, quiet : Bool) -> String

    Render the report as text.

    With a single data document the output is just the error lines, which is the friendly case for one-off checks. With several documents each line is prefixed by the document name and a summary line is appended, which is the useful case for CI.

    to_string

    fn to_string(error : ValidationError) -> String

    Render one error as a single line: $.user.age: expected Int, got String.

    usage_text

    fn usage_text() -> String

    Usage text, printed by --help and after a usage error.

    validate

    fn validate(schema : Type, value : Json) -> Array[ValidationError]

    Validate a JSON value against a schema.

    Returns all validation errors. An empty array means the value is valid. Undeclared fields of an object are ignored.

    validate_strings

    fn validate_strings(schema_text : String, data_text : String) -> Result[Array[ValidationError], String]

    Validate in one step from raw text.

    Takes a schema document and a data document, both as JSON text. Returns Ok(errors) where an empty list means the data is valid, or Err(reason) when either document could not be parsed.

    validate_text

    fn validate_text(schema : Type, data_text : String) -> Array[ValidationError]

    Validate a data document against an already parsed schema.

    Unlike validate_strings, a data document that is not valid JSON is reported as a single InvalidJson error instead of aborting the call, so callers that process many documents can keep going and report everything.

    value_name

    fn value_name(value : Json) -> String

    The JSON type name of a concrete value, used in error messages.

    version_text

    fn version_text() -> String

    Version text, printed by --version.