Library API

The CLI wraps one entry point. Require the gem and call Schematrix.generate:

require "schematrix"
require "schematrix/generators"

Schematrix.generate(
  generators: [
    Schematrix::Generators::PlainRuby.new("generated", "MyApp"),
    Schematrix::Generators::Rbs.new("sig", "MyApp"),
    Schematrix::Generators::AuxiliaryFiles.new("generated", "MyApp"),
  ],
  input_files: ["person.schema.json"],
)

Schematrix.generate compiles every schema reachable from the inputs and calls write on each generator per compiled class. It returns the compiled records, a hash keyed by schema locator (file:///path/to/person.schema.json#/properties/address) whose values are internal Schematrix::Schema data objects. Treat the values as opaque unless you are extending the tool.

Parameters

Parameter Type Meaning
generators array Generator instances, one per output format.
input_files array Schema document paths, JSON or YAML.
closed_shapes boolean Absent additionalProperties behaves as false. Default false.
schema_dirs array Directories remote schema URIs resolve through, in order. Default [].
max_compilation_work integer Compilation work limit. Default CompilationBudget::DEFAULT_LIMIT.

The library default for schema_dirs is empty: unlike the CLI, the library does not fall back to vendor/schemas. Pass the directories you want.

Errors

generate can raise:

Output-path and enum member collisions are checked before the first file is written. Conversion errors occur during rendering, so earlier files may already have been written.

Type signatures

The gem ships the Sorbet signatures of this API in rbi/schematrix.rbi. A project that typechecks with Sorbet gets them in two ways: the srb wrapper loads the rbi/ directory of every gem in the bundle, and tapioca merges that directory into the RBI it generates for the gem. Either way a call to Schematrix.generate or a generator constructor is checked against the parameter types above. The file covers the documented surface only: the entry point, the errors, ScalarType, the coercers, the generator classes and their public methods. Sorbet checks the file in CI, and a test checks that every method it declares exists on the loaded code with the same parameters.

Logging

Schematrix.logger is nil by default and the pipeline runs silent. Assign a logger to observe the run; the CLI assigns a TTY::Logger. The pipeline calls debug, info, warn, and fatal on it:

require "tty-logger"
Schematrix.logger = TTY::Logger.new

Driving a generator directly

Generator instances are usable without the compiler. Each takes an output directory and a module name:

generator = Schematrix::Generators::PlainRuby.new("out", "MyApp")

generator.target_path(locator) # => "out/person.rb", no side effects
generator.write(locator, node) # renders, formats, writes, returns the path

Run-once generators write auxiliary files once per run instead of one file per locator. Schematrix::Generators::AuxiliaryFiles is the one built-in: it takes the same constructor arguments and writes the shared CoercionHelper module that the data formats’ coercion methods call into. The pipeline hands it the whole compiled record set through write_run_once(objects), and run_once_target_paths lists the static paths it claims for collision detection:

objects =
  Schematrix.generate(
    generators: [],
    input_files: ["person.schema.json"],
  )

helper = Schematrix::Generators::AuxiliaryFiles.new("out", "MyApp")
helper.run_once_target_paths
# => ["out/coercion_helper.rb", "out/coercion_helper.rbs"]
helper.write_run_once(objects)

Enum modes

Every generator accepts enums: nil, :classes, or :unions. Other values raise ArgumentError at construction. This is a generator option, not a keyword to Schematrix.generate.

The pipeline collects explicit modes from all generators. Two distinct explicit modes raise ArgumentError. One explicit mode applies to the entire run, including generators that left it unset. With none set, any enum writer selects :classes; otherwise the mode is :unions.

The resolved mode belongs to the invocation. The pipeline passes it through write and transform without changing the generator’s configuration. Reusing an instance in another run cannot change a render already in progress. Concurrent runs must still use distinct output files. Direct write and transform calls accept an enums: override; otherwise they use the constructor’s mode or :unions.

The enum writers are PlainRubyEnums, SorbetRubyEnums, RbiEnums, and RbsEnums, registered as plain_ruby_enums, sorbet_ruby_enums, rbi_enums, and rbs_enums. They alone write enum classes:

Schematrix.generate(
  generators: [
    Schematrix::Generators::PlainRuby.new("generated", "MyApp"),
    Schematrix::Generators::PlainRubyEnums.new("generated", "MyApp"),
    Schematrix::Generators::AuxiliaryFiles.new("generated", "MyApp"),
  ],
  input_files: ["person.schema.json"],
)

:classes without an enum writer logs a warning asking for a *_enums generator. It does not fail, but the referenced enum constants must come from elsewhere for generated data classes and builders. Generated parsers resolve enum constants statically and do not need the classes loaded. With the default nil logger, no warning is visible. DSL and partial generators do not write enum files.

An enum writer with explicit :unions still writes enum classes; slots use union rendering without converting through those classes. Compilation registers qualifying enums whenever class usage or enum file generation is requested. The write loop sends enum records only to enum writers and all other records to the other per-class generators.

See Schema features for qualification, conversion, and format composition.

Scalar types

scalar_types customizes how scalar slots render, per notation, and which of them convert on load and dump:

Schematrix::Generators::SorbetRuby.new(
  "app/generated",
  "Billing",
  scalar_types: {
    ["string", "money"] => Schematrix::ScalarType.new(
      sorbet: "Money",
      rbs: "Money",
      coercer: Money
    ),
    ["integer", "id"] => Schematrix::ScalarType.new(
      sorbet: "Id",
      rbs: "Id"
    )
  }
)

The keys are [type, format] pairs; a lookup tries the exact pair, then the bare type, so a format with no entry renders as its bare type. Schematrix::ScalarType carries the Sorbet spelling, the RBS spelling, and an optional coercer: any named module answering coerce(value). A coercing slot follows the same rule the nested-class slots follow: a document value loads through coercer.coerce, an instance dumps through it, and anything else passes through. That conversion runs at every position the type algebra renders the formatted type: scalar properties, array elements and tuple positions, map and catch-all values, and union branches. The built-in table maps the standard formats this way (date to Date, date-time to Time) and emits the converters once, into the coercion helper module of the output directory; a project coercer is referenced by its name and required by the generate script, as any class it names is. A union whose branches convert the same JSON type through different coercers has no correct converter and fails the run with CoercionConflictError (see Limits).

A coercer must be a named module (or class) answering coerce: an anonymous module, a nameless value, or a missing method raises ArgumentError at generator construction, before any file is written.

ScalarType.new also accepts reader: nil. Supply a named module or class answering .read(node) to accept nonliteral Prism nodes in that format slot when parsing DSL source:

Schematrix::ScalarType.new(
  sorbet: "Money",
  rbs: "Money",
  coercer: Money,
  reader: MoneyReader,
)

The generator validates the reader at construction, just as it validates the coercer. The reader accessor returns the configured reader. The application must load the reader and its dependencies; generated parsers reference it by name. Readers are trusted code and are called only for nonliteral nodes at the configured formatted positions, including nested arrays, maps, tuples, and extra values. A reader must return the final document value, with no second coercion. JSON literal values pass through project coercers unchanged on both paths; the coercer runs when a setter receives a value that is not a JSON scalar. Reader failures become generated ParseError instances with the original exception as cause. Two distinct readers on one DSL value slot fail parser generation with CoercionConflictError. No automatic Date constructor reader is supplied.

Seventeen per-class generator classes exist: PlainRuby, SorbetRuby, Rbi, and Rbs for the data classes, plus DslPlainRuby, DslSorbetRuby, DslRbi, and DslRbs for the instance builders, DslParser for static DSL parsing, plus PartialPlainRuby, PartialSorbetRuby, PartialRbi, and PartialRbs for the partial classes, plus PlainRubyEnums, SorbetRubyEnums, RbiEnums, and RbsEnums for enum classes. They share the rendering described in Generators; the builders place every class under a Dsl module level and the partials under a Partial module level, so the families can write to one directory without colliding.

Generating a DSL parser

Schematrix.generate(
  generators: [Schematrix::Generators::DslParser.new("out", "MyApp")],
  input_files: ["person.json"],
)

This writes out/parser/person.rb and its support files, out/parser/runtime.rb and out/parser/parse_error.rb. No additional generator is needed. Require the person parser and call MyApp::Parser::Person.parse(source, filename: "(dsl)") or MyApp::Parser::Person.parse_file(path) to get a Hash. The module also supports inclusion and method overrides that call super.

The generated parser uses Prism 1.4 or later and the standard library. It does not load Schematrix, builders, or enum classes. See The instance DSL for the grammar and ParseError fields.

The generated loading model

require "schematrix" also loads Schematrix::Meta::Schema under lib/schematrix/meta/. That class is schematrix’s own loading model for schema documents, generated by rake meta:generate from the vendored 2020-12 metaschema. It is an implementation detail of the compiler, not a public surface. It is excluded from the API reference on purpose.