Add Reusable Schema Definitions With $defs And $ref#31
Merged
Conversation
Introduce SchemaResolver and extend load_schema with an optional shared resolver context. The resolver materializes Draft 2020-12 / references in the scalar and scalar-array subset already understood by nml-tools. Support same-document and local YAML/JSON references, standard JSON Pointer fragments, root-schema property composition, sibling bound/enum narrowing, annotation/default selection, and atomic array default-control replacement. Reject remote references, cycles, unsupported resource/anchor behavior, older declared dialects, unsupported composition, and derived-type object fields with diagnostics that retain use-site and target context. Instantiate one resolver per CLI command so generate, check, standalone subcommands, templates, f2py paths, and validation reuse loaded reference documents consistently without changing generator input interfaces.
Add validate_schema_defaults so defaults are checked against the effective scalar or array constraints before generation or input validation. This makes inline schemas and schemas expanded through follow the same correctness rules. Validate scalar defaults, item defaults, array pad values, array default extent/layout controls, runtime-resolved shapes, and the existing flex-array incompatibility. Invoke the check from Fortran, Markdown, template, and namelist validation paths so invalid defaults cannot render differently depending on command.
Document the Draft 2020-12 / subset, local-file resolution rules, use-site annotation/default behavior, array default bundle policy, and deliberately unsupported reference forms in the README. Add schema-layer tests for inline and external references, escaped JSON Pointer fragments, root composition, default override semantics, diagnostics, caching, cycles, dialect rejection, and cross-output equivalence. Add CLI tests for generation and validation through external definitions and validator regressions for invalid operational defaults.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds local JSON Schema Draft 2020-12 $defs / $ref support to nml-tools by introducing a schema resolution phase that expands supported references (including same-document and local file references) before generators and validation operate on the schema. It also tightens and centralizes “operational default” validation so defaults used by generation/templates are consistently checked.
Changes:
- Added
SchemaResolver,load_schema(..., resolver=...), andresolve_schema(...)to load/resolve$refand compose supported sibling keywords into a single effective schema. - Introduced
validate_schema_defaults(...)and wired it into validation + Fortran/Markdown/template generation to uniformly validate operational defaults. - Added extensive test coverage for reference resolution/composition and CLI behavior, and updated README docs.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/nml_tools/schema.py |
Implements local $defs/$ref resolution + deterministic composition rules for the supported schema subset. |
src/nml_tools/cli.py |
Uses a shared SchemaResolver per command invocation so referenced documents are cached consistently. |
src/nml_tools/validate.py |
Adds schema-default validation and calls it from validate_namelist. |
src/nml_tools/codegen_fortran.py |
Validates schema defaults early during context building to ensure generated initialization is correct. |
src/nml_tools/codegen_markdown.py |
Validates schema defaults to keep docs consistent with operational behavior. |
src/nml_tools/codegen_template.py |
Validates schema defaults before rendering filled/documented templates. |
tests/test_schema.py |
New test suite covering $ref resolution, composition behavior, caching, and cross-output equivalence. |
tests/test_validate.py |
Adds tests asserting invalid schema defaults are rejected by validation. |
tests/test_cli_config.py |
Adds CLI integration tests for external refs and for generation using referenced definitions. |
README.md |
Documents reusable definitions, local references, and composition/unsupported forms. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add examples/03_references with a local definition library, a chained external root reference, inline $defs, use-site sibling refinement, inherited and overridden defaults, and a runtime-sized referenced array. Check in generated Fortran, Markdown, and template artifacts so the example can be inspected without regeneration. Extend CI to verify those generated files and validate the filled namelist against both resolved schemas.
Recognize Windows drive-letter absolute paths as local external $ref targets before URI parsing, while continuing to reject URI and remote reference forms. Remove the unused reference identity accumulation state. Preserve the existing array default diagnostic phrase through centralized default validation and add regression tests for both Windows path spellings and diagnostic compatibility.
Reject reachable raw $ref nodes at the shared default-validation boundary with guidance to call load_schema() or resolve_schema(), so direct validation and generation APIs report the required normalization step clearly. Reject optional arrays that supply operational defaults without an object items schema, while retaining the existing diagnostic for default controls supplied without an array default. Add regression tests for both paths.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #30
This pull request adds local JSON Schema Draft 2020-12
$defsand$refsupport for the scalar and scalar-array schema model already implemented by
nml-tools.Schemas can now define reusable field specifications inline or in local
YAML/JSON definition files:
The new schema resolver expands supported references before any generator or
validator processes a schema. Fortran modules, Markdown documentation,
namelist templates, f2py/Python wrappers, kind maps, and namelist validation
therefore operate on one consistent effective schema.
Main Changes
SchemaResolver,load_schema(..., resolver=...), andresolve_schema(...)insrc/nml_tools/schema.py..yml,.yaml, and.jsonreference resolution.
tokens.
generate,check,gen-fortran,gen-markdown,gen-template, andvalidate, so referenced documents arecached consistently for one command invocation.
$refsibling composition for the supported nml-toolssubset:
propertiesandrequiredentries can be composed.Markdown generation, template generation, and namelist validation.
composition rules, and unsupported forms.
Reference Behavior
References may be:
#/$defs/positive_count;common-definitions.yml#/$defs/fraction;Relative paths are resolved from the document containing the
$ref, not fromthe current working directory or the nml-tools config file. More than one
definition document can be used without adding a registry to
nml-config.toml.The following reference locations are supported in this initial
implementation:
itemsschemas within supported arrays.References that would introduce nested object fields remain out of scope until
derived-type generation is implemented.
Sibling Composition And Defaults
Draft 2020-12 allows keywords next to
$ref. For the supported schema subset,the resolver materializes a single effective schema:
type,x-fortran-kind,x-fortran-len,x-fortran-shape, andx-fortran-flex-tail-dimsare inherited when omitted and rejected whenconflicting.
minimum,maximum,exclusiveMinimum, andexclusiveMaximumcombine tothe narrower interval; empty intervals are rejected.
enumvalues are intersected when a use site further restricts a reusabledefinition; empty intersections are rejected.
title,description,examples, anddefaultvalues are usedwhen provided; otherwise referenced annotations/defaults are inherited.
local fields are appended, matching fields are composed, and
requiredentries are combined.
For arrays, an array-level
defaultand itsx-fortran-default-order,x-fortran-default-repeat, andx-fortran-default-padkeywords are treated as one operational bundle. Ause-site array default replaces any referenced control bundle unless new
controls are supplied alongside that local default.
Validation Tightening
defaultaffects generated initialization and filled templates in nml-tools,so it is validated as operational input rather than left as an unchecked
annotation.
This pull request validates defaults uniformly for inline and referenced
schemas:
fixed-string-length constraints;
x-fortran-shapeand pad/repeat rules;This is an intentional correctness tightening: existing valid schemas are
unchanged, while previously accepted invalid defaults now fail consistently
across generation and validation entry points.
Rejected Or Deferred Forms
The first implementation deliberately rejects:
$id/$anchor-based base resolution;$dynamicRefand$dynamicAnchor;allOf,anyOf,oneOf,not, and conditional composition;definitionsas an alias for$defs;If a schema using references declares a JSON Schema dialect, it must declare
Draft 2020-12. Schemas without
$schemaremain accepted.Tests
Added focused test coverage for:
$defs/$refresolution;useful unresolved-reference diagnostics;
Python wrappers, f2py kind maps, and namelist validation;
validate --schemausing external definitions withconstants and runtime dimensions;