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:
-
Schematrix::InvalidSchemaErrorwhen a document parses to something that is neither an object nor a boolean. -
Schematrix::UnsupportedDialectErrorwhen a$schemaheader names a dialect other than draft 2020-12. -
Schematrix::UnresolvableLocatorErrorwhen a$refor$dynamicReftarget cannot be found, a remote URI has no local file, or a URI scheme is unsupported. -
Schematrix::OutputCollisionErrorwhen two compiled classes, or two generators sharing a file extension in one directory, would write the same file. Enum member folding collisions also raise this error, as do member names equal to the enclosing class leaf in the Sorbet Ruby and RBI enum generators (plain Ruby and RBS permit them). -
Schematrix::CoercionConflictErrorwhile rendering a class whose schema unions branches that would convert the same JSON type through different coercers (see Limits). -
Schematrix::CoercionHelperCollisionErrorwhile rendering a class whose coerced property names fold onto the generated coercion helpers.
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
-
locatoris the URI string of a compiled object schema. -
nodeis theSchematrix::Schemarecord for that locator. -
Pass
format: falseto skip SyntaxTree formatting and receive the template output as-is. -
Pass
command:to name the regenerating command in the written files’ banner; the CLI passesschematrix, the rake taskrake meta:generate. -
Pass
enums: :classesorenums: :unionsto select enum usage for the whole run. The defaultnilleaves mode selection toSchematrix.generate; see Enum modes. -
Pass
scalar_types:to map[type, format]pairs to renderings of the project’s own, merged over the built-in table (below). -
Pass
immutable: trueto write data classes as frozen value objects: readers instead of accessors, awithcopier, and value equality. The data generators accept it; the DSL builders and parser refuse it. See Immutable classes. -
Pass
optionals: :maybeto type every optional property asMaybe[T]instead of a nilable type, keeping absent and null apart through dump. Passmaybe:with a project module that answersfrom(value)andempty, and whose instances answerpresent?andwhen_present, and the generated code calls through it; without it the generator writes its ownMaybemodule once per output directory. The DSL builders and parser refuse the option. See Maybe optionals.
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.