schematrix
Generate Ruby code and RBS signatures from JSON Schema definitions.
schematrix is a Ruby gem and CLI tool that reads JSON Schema files and produces idiomatic Ruby source code. Point it at a schema, choose a generator, and get ready-to-use Ruby classes and type signatures written into your project.
Seventeen per-class output formats share one compiler: plain Ruby classes, Ruby classes with inline Sorbet signatures, Sorbet RBI files, RBS signature files, and each of those four notations again as a builder DSL, a partial class family, and enum classes, plus a standalone DSL parser. The DSL modules define instances of the schema through plain method calls, and a project mixes them into its own classes; running one builds the document as a Hash, and parsing one reconstructs the same document statically.
Qualifying string and integer enums can generate named member classes with the four *_enums generators, or render as literal unions in RBS. See Enums.
Requirements
-
Ruby ≥ 3.4
-
Bundler (for gem installation)
Installation
Add schematrix to your Gemfile:
gem 'schematrix', '~> 0.1'
Then run:
bundle install
Or install it directly:
gem install schematrix
Quick Start
Given this JSON Schema (person.schema.json):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["firstName", "lastName"]
}
Run:
schematrix -g plain_ruby -g auxiliary_files -m MyApp -o app/generated person.schema.json
This produces app/generated/person.rb plus the shared app/generated/coercion_helper.rb module (see generators):
# DO NOT EDIT MANUALLY # This is an autogenerated file for the Person class. # Please instead update this file by running `schematrix`. module MyApp class Person ORIGINAL_KEYS = { "firstName" => :first_name, "lastName" => :last_name, "age" => :age }.freeze def initialize(first_name:, last_name:, age: nil) @first_name = first_name @last_name = last_name @age = age @_extra = {} end attr_accessor :first_name, :last_name, :age def [](key) @_extra[key] end def []=(key, value) @_extra[key] = value end def self.from_h(hash) instance = new( first_name: hash["firstName"], last_name: hash["lastName"], age: hash["age"] ) hash.each do |key, value| next if ORIGINAL_KEYS.key?(key) instance[key] = value end instance end def to_h result = {} result["firstName"] = @first_name unless @first_name.nil? result["lastName"] = @last_name unless @last_name.nil? result["age"] = @age unless @age.nil? @_extra.each { |key, value| result[key] = value } result end end end
Every generated class carries ORIGINAL_KEYS, the map from the schema’s property names to the generated accessor symbols, plus a from_h/to_h pair that loads and dumps documents under those original names: nested object schemas load through their own from_h, unknown keys ride the catch-all ([]/[]=) with their original String keys, and absent keys keep their constructor defaults. See the generated code guide.
Schema documents in YAML work the same way; JSON is a subset of YAML and both load.
CLI Reference
Usage: schematrix [OPTIONS] INPUT [INPUT...]
Generate code matching a JSON Schema
Options:
--closed-shapes Treat absent additionalProperties as false: closed
classes with no catch-all, instead of the spec
default
--enums string Enum slot types: classes or unions (default depends
on generators)
-g, --generators list Output generators to use, e.g.: plain_ruby,
auxiliary_files
-h, --help Print usage
--immutable Generate data classes as frozen value objects:
readers, a with copier, and value equality; data
generators only
-m, --module string Module where the output code will be placed
-o, --output string Directory where the output will be written
--schema-dir string Directory where remote schema URIs resolve to local
files; may be given multiple times, defaults to
vendor/schemas
-v, --verbose Each additional occurrence lowers logger level,
-vvv for the minimum
Examples:
Produce plain Ruby code for the given schema
$ schematrix -g plain_ruby -m MyModule -o /output/dir my-json-schema.json
The options list above mirrors schematrix --help. The full option reference, including output directory pairing and exit behavior, lives in the command line manual.
Example usages
Generate plain Ruby classes for a single schema:
schematrix -g plain_ruby -g auxiliary_files -m MyApp -o app/generated schema.json
Generate a standalone DSL parser, with its support files included:
schematrix -g dsl_parser -m MyApp -o out input.json
The generated parser needs Prism 1.4 or later, but no Schematrix runtime. See The instance DSL for the accepted grammar and reader extensions.
Process multiple schemas at once:
schematrix -g rbs -m Contracts user.schema.json order.schema.json
Or even:
schematrix -g plain_ruby -g auxiliary_files -m Contracts *.schema.json
You might specify different output directories for different generators like so:
schematrix -g plain_ruby -o generated -g rbi -o rbi -g auxiliary_files -o generated *.schema.json
Resolving remote schemas
schematrix never fetches anything over the network. When a schema references another schema through an http(s) URI, that URI must resolve to a local file through a schema directory. By default the tool searches vendor/schemas in the current directory; pass --schema-dir to point somewhere else:
schematrix -g plain_ruby -m MyApp --schema-dir local/schemas schema.json
The option may be given multiple times; the directories are searched in order, and explicit directories replace the default rather than adding to it.
The directory mirrors the URI structure: host, then path. The URI
https://json-schema.org/draft/2020-12/meta/core
resolves to the first existing file among
vendor/schemas/json-schema.org/draft/2020-12/meta/core vendor/schemas/json-schema.org/draft/2020-12/meta/core.json vendor/schemas/json-schema.org/draft/2020-12/meta/core.yaml vendor/schemas/json-schema.org/draft/2020-12/meta/core.yml
Directories given more than once are searched in order, and the first matching file wins. A remote URI with no matching local file is an error listing every candidate path. The 2020-12 JSON Schema meta files are vendored in this repository under vendor/schemas, so schemas referencing https://json-schema.org/draft/2020-12/... resolve out of the box when running from the repository root.
Generators
The data, DSL, and partial generators write object schema files. The four enum generators write qualifying enum schema files:
| Name | Writes |
|---|---|
plain_ruby |
Mutable Ruby classes, no annotations |
sorbet_ruby |
Mutable Ruby classes with inline Sorbet sig blocks |
rbi |
Sorbet signature files: signatures only, empty bodies |
rbs |
RBS signature files |
dsl_plain_ruby |
Instance DSL builders, no annotations |
dsl_sorbet_ruby |
Instance DSL builders with inline Sorbet sig blocks |
dsl_rbi |
Sorbet signature files for the instance DSL builders |
dsl_rbs |
RBS signature files for the instance DSL builders |
dsl_parser |
Prism parsers for DSL source, without executing the source |
partial_plain_ruby |
Partial classes, every property wrapped in the Maybe type |
partial_sorbet_ruby |
Partial classes with inline Sorbet sig blocks |
partial_rbi |
Sorbet signature files for the partial classes |
partial_rbs |
RBS signature files for the partial classes |
plain_ruby_enums |
Enum classes with frozen members, no runtime dependency |
sorbet_ruby_enums |
T::Enum classes with inline Sorbet signatures |
rbi_enums |
Sorbet enum declarations |
rbs_enums |
RBS declarations for the plain enum API |
See the generator guide for sample output and the type mapping tables per notation. Class names, module nesting, and file paths derive from the schema as described in Names and files. The instance DSL has its own page: The instance DSL.
Library usage
The CLI wraps one method. Require the gem, build generator instances, 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"], )
Collisions (two classes on one file) are detected before the first write, and the compile raises instead of producing partial output. See the library manual for the parameters, the error types, and logging. The gem exports the Sorbet signatures of this API in rbi/, which tapioca picks up.
How It Works
flowchart LR
A["JSON Schema files"] --> B["Compiler"]
B -- "Visits every schema reachable from the inputs and builds an internal representation of each object it finds" --> C["Generators\nlib/schematrix/generators/*"]
C -- "Transform each object into code and write one file per object into the output directory" --> D["Output files\ne.g. generated/person.rb"]
The core components are:
-
Schematrix::Compiler: drives compilation. Starting from the input files, it turns every reachable object schema into an internal representation. -
Schematrix::DocumentStore: loads and caches schema documents, and looks up the nodes they contain. -
Schematrix::Visitor: walks a schema subtree depth-first, resolvingrequiredfields and dispatching ontype. -
Schematrix::Schema: the internal representation of a schema node. -
Schematrix::Generators::*: the code generators; they handle source code production, formatting, and file I/O. -
Schematrix::CLI: the command-line interface, built ontty-option.
Documentation
The complete manual lives in docs/:
The API reference is generated with RDoc: run bundle exec rake rdoc and open doc/index.html.
Development
Clone the repository:
git clone https://github.com/cinzatech/schematrix.git cd schematrix bundle install
Run the CLI from the project root:
bundle exec exe/schematrix -g plain_ruby -m Test -o /tmp/generated examples/basic.schema.json
Run the test suite:
bundle exec rake test
Format and lint:
bundle exec stree check lib test exe rbi Rakefile bundle exec rubocop SRB_SKIP_GEM_RBIS=1 bundle exec srb tc
Contributing
Please, feel free to send Pull Requests. Machine-generated code is acceptable as long as it meets our standards for code quality and there’s at least one human behind who understands the code being sent. We believe in code as theory building, so we want contributions that strengthen the shared mental model, not weaken it.
License
schematrix is released under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).