Add continuum-schema-instance: build JSON instances from a JSON Schema

The Rust equivalent of json-schema-faker, scoped to what's actually
useful in Rust: resolves $ref (relative to whichever file references
it)/allOf, fills const/enum/default, and builds a value only for
properties listed in required - everything else is left out. For code
that has to construct a raw serde_json::Value against a schema it
doesn't own, instead of a typed struct.

Verified against continuum-schemas' real bambulab schemas manually, and
against local fixtures in tests (round-tripped through the jsonschema
crate to confirm the output actually validates).
This commit is contained in:
2026-09-05 16:59:12 +00:00
parent c452cf0d78
commit fed19b260f
7 changed files with 384 additions and 0 deletions
@@ -0,0 +1,64 @@
use std::path::PathBuf;
use continuum_schema_instance::build_instance_from_file;
use serde_json::json;
fn fixture(relative: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(relative)
}
/// Validates `instance` against the real schema file - closes the loop:
/// this confirms the builder's output is actually accepted by a real JSON
/// Schema validator (the `jsonschema` crate), not just "looks right".
fn assert_valid(schema_path: &PathBuf, instance: &serde_json::Value) {
let schema_text = std::fs::read_to_string(schema_path).unwrap();
let mut schema: serde_json::Value = serde_json::from_str(&schema_text).unwrap();
// These fixtures have no `$id` (real schemas in continuum-schemas do,
// as an absolute https:// URL) - the `jsonschema` crate resolves a
// relative `$ref` against the schema's own `$id`, so give it one
// pointing at this file's real location for the relative `$ref`s
// inside to resolve from during this check.
let absolute_path = schema_path.canonicalize().unwrap();
schema["$id"] = json!(format!("file://{}", absolute_path.display()));
let validator = jsonschema::validator_for(&schema).expect("schema itself should compile");
let errors: Vec<_> = validator.iter_errors(instance).collect();
assert!(errors.is_empty(), "instance {instance} did not validate: {errors:?}");
}
#[test]
fn required_fields_are_filled_and_optional_fields_are_left_out() {
let path = fixture("events/item_created.schema.json");
let mut instance = build_instance_from_file(&path).unwrap();
// "type" has no const at the envelope level, but item_created's own
// allOf branch overrides it to one - that const wins.
assert_eq!(instance["type"], json!("item_created"));
// "event_id"/"item_id" are required with no const - each gets a
// placeholder, ready to be overridden.
assert!(instance.get("event_id").is_some());
assert!(instance.get("item_id").is_some());
// "note" is optional and correctly left out entirely.
assert_eq!(instance.as_object().unwrap().len(), 3);
assert_valid(&path, &instance);
// The one field the caller actually has a value for - plain
// serde_json::Value indexing, no special "overrides" API needed.
instance["item_id"] = json!("item-42");
assert_valid(&path, &instance);
}
#[test]
fn allof_merge_lets_a_later_branch_override_an_earlier_ones_property() {
let path = fixture("events/item_deleted.schema.json");
let instance = build_instance_from_file(&path).unwrap();
// "type" is defined twice across the allOf: a plain string in the
// shared envelope, then overridden to `const: "item_deleted"` in this
// event's own branch - the later branch has to win.
assert_eq!(instance["type"], json!("item_deleted"));
assert_eq!(instance.as_object().unwrap().len(), 2); // event_id, type
assert_valid(&path, &instance);
}
@@ -0,0 +1,11 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "EventEnvelope",
"type": "object",
"properties": {
"event_id": { "type": "string" },
"type": { "type": "string" },
"note": { "type": "string" }
},
"required": ["event_id", "type"]
}
@@ -0,0 +1,15 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "ItemCreatedEvent",
"allOf": [
{ "$ref": "../common/envelope.schema.json" },
{
"type": "object",
"properties": {
"type": { "const": "item_created" },
"item_id": { "type": "string" }
},
"required": ["item_id"]
}
]
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "ItemDeletedEvent",
"allOf": [
{ "$ref": "../common/envelope.schema.json" },
{
"type": "object",
"properties": {
"type": { "const": "item_deleted" }
}
}
]
}