Sign in

    openrpc

    OpenRPC document modeling and validation for MoonBit

    openrpc
    json-rpc
    api-contract
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    23

    No dependencies

    #OpenRPC.mbt

    OpenRPC document modeling and validation for MoonBit.

    Language: MoonBit · License: Apache-2.0 · Package: zmjknn/openrpc

    verify

    This project fills a narrow gap between a JSON-RPC transport and an application: a service can publish an OpenRPC document, and MoonBit code can parse it into a typed contract with diagnostics that point back to the document. It is a library first; it does not replace JSON-RPC transports or MCP runtimes.

    #What works today

    • Parses OpenRPC 1.x document metadata, including tags, servers, server variables, method summaries, and examples.
    • Preserves reusable schemas, examples, pairings, content descriptors, errors, links, and tags under components.
    • Resolves local JSON Pointer references in method, parameter, result, tag, and example entries.
    • Validates parameter and result schemas that use nested local component references.
    • Validates parameter, result, and pairing examples against their declared schemas.
    • Expands declared server URL variables using caller overrides or defaults and enforces enum values.
    • Compares two parsed documents and classifies method, parameter, result, server, tag, and component changes.
    • Parses JSON into the required OpenRPC document, info, and method metadata.
    • Preserves ordered parameters (name, required, and raw schema) and optional result descriptors.
    • Accepts OpenRPC 1.x documents and rejects other major versions.
    • Rejects duplicate method names and reports invalid JSON, missing fields, wrong field types, and non-object methods with JSON-style paths.
    • Supports deterministic lookup with Document::find_method.
    • Builds a JSON-RPC 2.0 request from a parsed method with the caller's request id and named parameters.
    • Builds a positional JSON-RPC 2.0 request from the method's declared parameter order.
    • Rejects missing required parameters and names that are not declared by the method before a request is sent.
    • Rejects missing required positional parameters and excess positional values while allowing omitted trailing optional parameters.
    • Builds named or positional JSON-RPC notifications that omit id and reuse the same contract checks.
    • Builds contract-aware JSON-RPC batches from named or positional calls, checking method names and unique request ids.
    • Decodes a JSON-RPC 2.0 response for an expected request id into a raw result or structured error.
    • Correlates JSON-RPC batch responses with the original calls, reorders them deterministically, and preserves per-method result/error records.
    • Rejects malformed response envelopes, mismatched ids, and invalid error objects with JSON-style diagnostics.
    • Provides a pure, portable JSON Schema subset validator for boolean schemas, primitive type, enum, const, object properties, arrays, string and numeric bounds, and allOf/anyOf/oneOf/not composition.
    • Exposes opt-in Parameter::validate, Method::validate_named_params, Method::validate_positional_params, Method::validate_result, and ResultDescriptor::validate helpers with stable JSON-style paths.
    • Lints parsed contracts for empty names, duplicate parameters, ambiguous positional ordering, malformed or contradictory schema boundaries, and invalid nested composition.
    • Keeps the result embeddable on MoonBit targets; there is no native-only file or network dependency.

    #Small example

    import { "zmjknn/openrpc" @openrpc }

    let source = #|{
    #| "openrpc": "1.4.0",
    #| "info": {"title": "Inventory", "version": "1.0.0"},
    #| "methods": [{"name": "inventory.list"}]
    #|}

    match @openrpc.Document::parse(source) {
    Ok(document) => println(document.methods[0].name)
    Err(diagnostics) => {
    for diagnostic in diagnostics {
    println(diagnostic.path + ": " + diagnostic.message)
    }
    }
    }

    #Install from Mooncakes

    After the maintainer publishes a release, add the module to a MoonBit project:

    moon add zmjknn/openrpc

    Then import it as @openrpc as shown above. The repository quickstart can be run before publication with moon run examples/quickstart --target wasm-gc.

    #Runnable quickstart

    The repository includes an executable consumer under examples/quickstart. Run it from the repository root:

    moon run examples/quickstart --target wasm-gc

    Expected output:

    method=inventory.get, server=https://dev.inventory.example/rpc, request={"jsonrpc":"2.0","id":7,"method":"inventory.get","params":{"id":"abc"}}

    The same contract is exercised by submission_readiness_test.mbt, so the README example and the library behavior are tested together.

    After selecting a method, request construction stays pure and transport-agnostic:

    let params : Map[String, Json] = {"id": "abc"}
    let request = method_value.build_request(7, params)

    The result is a JSON-RPC 2.0 envelope with an object-valued params field. When a server expects positional parameters, the ordered contract can be used directly:

    let values : Array[Json] = ["abc"]
    let request = method_value.build_positional_request(7, values)

    Positional values are a prefix of the declared list, so only trailing optional parameters may be omitted. Schema-value validation and transport dispatch remain separate concerns.

    Schema validation is explicit when an application wants a contract check at its boundary:

    let params : Map[String, Json] = {"id": "abc"}
    match method_value.validate_named_params(params) {
    Ok(_) => println("parameters satisfy the OpenRPC schemas")
    Err(diagnostics) => println(diagnostics[0].path + ": " + diagnostics[0].message)
    }

    The validator intentionally covers a portable JSON Schema subset. Document-aware validation resolves local references, including references nested below object properties and composition keywords. Circular references are reported during value validation. The library does not load external schemas, evaluate regular-expression pattern or format, coerce values, or apply defaults. Request builders remain schema-agnostic unless one of the opt-in validation helpers is called.

    For component-backed schemas, validate through the parsed document:

    match document.validate_named_params("inventory.get", {"id": "abc"}) {
    Ok(_) => println("parameters satisfy the referenced schemas")
    Err(diagnostics) => println(diagnostics[0].path + ": " + diagnostics[0].message)
    }

    Server URL variables can be expanded without opening a connection:

    match document.find_server("primary") {
    Some(server) =>
    match server.render_url({"region": "staging"}) {
    Ok(url) => println(url)
    Err(diagnostics) => println(diagnostics[0].message)
    }
    None => println("server is not declared")
    }

    Use Document::diff to compare revisions. Each change includes a path, kind, and compatibility impact. Document::resolve_ref resolves local JSON Pointers such as #/components/schemas/Item; external URLs are rejected, and the library never reads files or performs network requests.

    Run the semantic pass after parsing when a document is being published or registered:

    let diagnostics = document.lint()
    for diagnostic in diagnostics {
    println(diagnostic.path + ": " + diagnostic.message)
    }

    Linting is deterministic and non-mutating. It catches duplicate parameter names, a required positional parameter after an optional one, empty declaration names, and contradictory or malformed bounds below nested properties, items, and composition schemas.

    For one-way calls, use a notification builder; it intentionally emits no id and therefore has no response to decode:

    let notification = method_value.build_notification({"scope": "all"})

    For several response-bearing calls, let the document resolve each method and compose one batch:

    let calls : Array[@openrpc.BatchCall] = [
    @openrpc.BatchCall::new(
    1,
    "inventory.get",
    @openrpc.RequestParams::named({"id": "abc"}),
    ),
    ]
    let batch = document.build_batch(calls)

    Batch diagnostics include the failing call index, and duplicate request ids are rejected before the JSON array is emitted. Notifications remain a separate API because they do not participate in response matching.

    When a server returns a batch, pass the original calls back to the document to correlate out-of-order responses:

    match document.decode_batch_response(response, calls) {
    Ok(values) => {
    for value in values {
    println(value.method_name)
    }
    }
    Err(diagnostics) => println(diagnostics[0].path + ": " + diagnostics[0].message)
    }

    The correlator reports unknown, duplicate, and missing ids before returning a partial result. It delegates each envelope to Method::decode_response, so single-response and batch-response validation share the same JSON-RPC rules.

    When a transport returns a JSON value, the same method can validate the response envelope without performing I/O:

    match method_value.decode_response(response, 7) {
    Ok(@openrpc.RpcResponse::Result(value)) => println(value.stringify())
    Ok(@openrpc.RpcResponse::Error(error_value)) => println(error_value.message)
    Err(diagnostics) => {
    for diagnostic in diagnostics {
    println(diagnostic.path + ": " + diagnostic.message)
    }
    }
    }

    The decoder matches the request id, enforces the JSON-RPC 2.0 envelope, and preserves result/error payloads as JSON. It does not evaluate the method's opaque schema.

    #Repository structure

    The implementation boundaries and package layout are documented in docs/architecture.md. The parser and message model live in openrpc.mbt; metadata parsing, local reference resolution, contract diffs, schema validation, and semantic linting are separated into focused modules. The quickstart demonstrates a separate executable consumer.

    #Deliberate boundary

    The library covers the document model, local reference resolution, semantic linting, schema validation, contract comparison, and transport-free request/response/notification/batch boundaries. It does not open sockets, load remote files, generate SDK code, or provide a web editor.

    #Development

    The public history follows the project workflow: Issue, feature branch, failing test, implementation, regression test, pull request, CI, and merge.

    moon fmt --check moon check --target wasm-gc moon build --target wasm-gc moon test --target wasm-gc moon run examples/quickstart --target wasm-gc

    GitHub Actions runs the same format, check, build, test, and quickstart steps. The public development record is maintained by zmjknn through Issues, feature branches, failing tests, implementation commits, PR review, CI, and merge.

    #Mooncakes publication

    The module metadata and package contents are prepared for Mooncakes. The maintainer-controlled login and publish steps are documented in docs/mooncakes-publish.md; no credential is stored in this repository.

    The repository is maintained by zmjknn for the MoonBit September 2026 hackathon.

    BatchCall

    pub struct BatchCall {
    id : Json
    method_name : String
    params : RequestParams
    }

    BatchCall::new

    fn BatchCall::new(id : Json, method_name : String, params : RequestParams) -> BatchCall

    BatchResponse

    pub struct BatchResponse {
    id : Json
    method_name : String
    response : RpcResponse
    }

    A response correlated with one of the calls in a JSON-RPC batch.

    ChangeImpact

    pub enum ChangeImpact {
    Breaking
    Compatible
    Informational
    }

    Severity assigned to a detected contract change.

    ChangeKind

    pub enum ChangeKind {
    MethodAdded
    MethodRemoved
    ParameterAdded
    ParameterRemoved
    ParameterRequiredChanged
    ParameterOrderChanged
    ParameterSchemaChanged
    ResultAdded
    ResultRemoved
    ResultSchemaChanged
    ServerAdded
    ServerRemoved
    ServerChanged
    TagAdded
    TagRemoved
    ComponentAdded
    ComponentRemoved
    ComponentChanged
    }

    Kind of contract change detected by Document::diff.

    Components

    pub struct Components {
    schemas : Map[String, Json]
    examples : Map[String, Json]
    example_pairings : Map[String, Json]
    content_descriptors : Map[String, Json]
    errors : Map[String, Json]
    links : Map[String, Json]
    tags : Map[String, Json]
    }

    Reusable OpenRPC components. Values remain JSON so unsupported schema keywords are preserved instead of discarded.

    Components::empty

    fn Components::empty() -> Components

    Create an empty component collection.

    Components::find

    fn Components::find(self : Components, section : String, name : String) -> Json?

    Return a named component from a recognized OpenRPC component section.

    ContractChange

    pub struct ContractChange {
    path : String
    kind : ChangeKind
    impact : ChangeImpact
    summary : String
    }

    One machine-readable change between two parsed OpenRPC documents.

    Diagnostic

    pub struct Diagnostic {
    path : String
    code : DiagnosticCode
    message : String
    }

    DiagnosticCode

    pub enum DiagnosticCode {
    InvalidJson
    ExpectedObject
    MissingField
    InvalidField
    UnsupportedVersion
    DuplicateMethod
    MissingParameter
    UnknownParameter
    TooManyParameters
    EmptyMethodName
    EmptyParameterName
    EmptyResultName
    DuplicateParameter
    OptionalBeforeRequired
    InvalidSchemaBoundary
    EmptyBatch
    UnknownMethod
    DuplicateRequestId
    SchemaViolation
    UnsupportedSchema
    InvalidResponse
    InvalidBatchResponse
    UnknownResponseId
    DuplicateResponseId
    MissingResponse
    ResponseIdMismatch
    InvalidRpcError
    DuplicateTag
    UnsupportedReference
    UnresolvedReference
    CircularReference
    InvalidServerTemplate
    UnknownServerVariable
    InvalidServerVariableValue
    InvalidExamplePairing
    }

    Document

    pub struct Document {
    openrpc : String
    info : Info
    methods : Array[Method]
    servers : Array[Server]
    tags : Array[Tag]
    components : Components
    source : Json
    }

    Document::build_batch

    fn Document::build_batch(self : Document, calls : Array[BatchCall]) -> Result[Json, Array[Diagnostic]]

    Document::decode_batch_response

    fn Document::decode_batch_response(self : Document, response : Json, calls : Array[BatchCall]) -> Result[Array[BatchResponse], Array[Diagnostic]]

    Decode and correlate a JSON-RPC batch response with its original calls.

    Servers may return responses in any order. The returned array is reordered to match calls, making downstream result handling deterministic while preserving each method name and structured error payload.

    Document::diff

    fn Document::diff(self : Document, next : Document) -> Array[ContractChange]

    Compare this document with a newer revision and return changes in source declaration order. Schema changes are conservatively marked as breaking.

    Document::find_method

    fn Document::find_method(self : Document, name : String) -> Method?

    Document::find_server

    fn Document::find_server(self : Document, name : String) -> Server?

    Find a declared server by its stable name.

    Document::find_tag

    fn Document::find_tag(self : Document, name : String) -> Tag?

    Find a document-level tag by its name.

    Document::lint

    fn Document::lint(self : Document) -> Array[Diagnostic]

    Semantic diagnostics for an already parsed OpenRPC document.

    Parsing answers whether a document has the expected JSON shape. Linting is a separate, non-mutating pass for rules that need the complete declaration context, such as positional optionality and recursive schema boundaries.

    Document::lint_references

    fn Document::lint_references(self : Document) -> Array[Diagnostic]

    Check all reference objects in the source document without rewriting it.

    Document::parse

    fn Document::parse(source : String) -> Result[Document, Array[Diagnostic]]

    Document::resolve_ref

    fn Document::resolve_ref(self : Document, reference : String) -> Result[Json, Array[Diagnostic]]

    Resolve one local JSON Pointer from the original document.

    External references are rejected so resolution never reads files or performs network requests. A pointer that targets another Reference Object returns that object; callers may resolve its "$ref" separately.

    Document::validate_examples

    fn Document::validate_examples(self : Document, method_name : String) -> Result[Unit, Array[Diagnostic]]

    Validate every example pairing attached to a method.

    Parameter examples are checked against the declared parameter order; result examples are checked against the declared result schema.

    Document::validate_named_params

    fn Document::validate_named_params(self : Document, method_name : String, params : Map[String, Json]) -> Result[Unit, Array[Diagnostic]]

    Validate a method's named parameters, resolving local schema references.

    Document::validate_positional_params

    fn Document::validate_positional_params(self : Document, method_name : String, values : Array[Json]) -> Result[Unit, Array[Diagnostic]]

    Validate positional method parameters with local schema reference support.

    Document::validate_result

    fn Document::validate_result(self : Document, method_name : String, value : Json) -> Result[Unit, Array[Diagnostic]]

    Validate a method result against its declared schema with local references.

    Document::validate_schema

    fn Document::validate_schema(self : Document, value : Json, schema : Json) -> Result[Unit, Array[Diagnostic]]

    Validate a JSON value against a schema after expanding its local references.

    Circular schema references are reported instead of being silently treated as empty schemas. The validator remains transport-independent and never resolves a reference outside the original document.

    Example

    pub struct Example {
    name : String
    summary : String?
    description : String?
    value : Json
    }

    An inline JSON example attached to a parameter, result, or pairing.

    ExamplePairing

    pub struct ExamplePairing {
    name : String
    description : String?
    params : Array[Example]
    result : Example?
    }

    A parameter-to-result example for one method.

    Info

    pub struct Info {
    title : String
    version : String
    description : String?
    }

    Method

    pub struct Method {
    name : String
    params : Array[Parameter]
    result : ResultDescriptor?
    summary : String?
    description : String?
    deprecated : Bool
    tags : Array[Tag]
    servers : Array[Server]
    examples : Array[ExamplePairing]
    }

    Method::build_notification

    fn Method::build_notification(self : Method, params : Map[String, Json]) -> Result[Json, Array[Diagnostic]]

    Method::build_positional_notification

    fn Method::build_positional_notification(self : Method, values : Array[Json]) -> Result[Json, Array[Diagnostic]]

    Method::build_positional_request

    fn Method::build_positional_request(self : Method, id : Json, values : Array[Json]) -> Result[Json, Array[Diagnostic]]

    Method::build_request

    fn Method::build_request(self : Method, id : Json, params : Map[String, Json]) -> Result[Json, Array[Diagnostic]]

    Method::decode_response

    fn Method::decode_response(_self : Method, response : Json, request_id : Json) -> Result[RpcResponse, Array[Diagnostic]]

    Method::has_tag

    fn Method::has_tag(self : Method, name : String) -> Bool

    Check whether a method declares a tag with the requested name.

    Method::validate_named_params

    fn Method::validate_named_params(self : Method, params : Map[String, Json]) -> Result[Unit, Array[Diagnostic]]

    Validate named parameters against declarations and their schemas.

    Method::validate_positional_params

    fn Method::validate_positional_params(self : Method, values : Array[Json]) -> Result[Unit, Array[Diagnostic]]

    Validate positional parameters against declarations and their schemas.

    Method::validate_result

    fn Method::validate_result(self : Method, value : Json) -> Result[Unit, Array[Diagnostic]]

    Validate a result value when this method declares a result schema.

    Parameter

    pub struct Parameter {
    name : String
    required : Bool
    schema : Json
    description : String?
    examples : Array[Example]
    }

    Parameter::validate

    fn Parameter::validate(self : Parameter, value : Json) -> Result[Unit, Array[Diagnostic]]

    Validate one value against this parameter's declared schema.

    Validation is explicit and opt-in. Request builders intentionally keep their schema-agnostic behavior so callers can choose when validation is appropriate for their transport or trust boundary.

    RequestParams

    pub enum RequestParams {
    Named(Map[String, Json])
    Positional(Array[Json])
    }

    RequestParams::named

    fn RequestParams::named(params : Map[String, Json]) -> RequestParams

    RequestParams::positional

    fn RequestParams::positional(values : Array[Json]) -> RequestParams

    ResultDescriptor

    pub struct ResultDescriptor {
    name : String
    schema : Json
    description : String?
    examples : Array[Example]
    }

    ResultDescriptor::validate

    fn ResultDescriptor::validate(self : ResultDescriptor, value : Json) -> Result[Unit, Array[Diagnostic]]

    Validate a result value against its declared schema.

    RpcError

    pub struct RpcError {
    code : Int
    message : String
    data : Json?
    }

    RpcResponse

    pub enum RpcResponse {
    Result(Json)
    Error(RpcError)
    }

    Server

    pub struct Server {
    name : String
    url : String
    description : String?
    variables : Map[String, ServerVariable]
    }

    A transport-neutral server descriptor from an OpenRPC document.

    Server::render_url

    fn Server::render_url(self : Server, overrides : Map[String, String]) -> Result[String, Array[Diagnostic]]

    Expand a server URL template using caller values or declared defaults.

    Values are inserted literally; callers remain responsible for URL encoding values that contain reserved URL characters. This function performs no I/O.

    ServerVariable

    pub struct ServerVariable {
    default_value : String
    enum_values : Array[String]
    description : String?
    }

    One variable in an OpenRPC server URL template.

    Tag

    pub struct Tag {
    name : String
    description : String?
    }

    A named OpenRPC tag used to group methods.

    quickstart_source

    fn quickstart_source() -> String

    The contract used by the reproducible quickstart package.

    quickstart_summary

    fn quickstart_summary() -> Result[String, Array[Diagnostic]]

    Produce the deterministic summary printed by the quickstart executable.

    validate_schema

    fn validate_schema(value : Json, schema : Json) -> Result[Unit, Array[Diagnostic]]

    Validate a JSON value against the supported OpenRPC Schema subset.

    The validator is deliberately pure: it does not resolve references, load files, coerce values, or perform network I/O. A schema may be a boolean schema or an object containing the supported JSON Schema keywords.