Add JSON Schema documentation for bambulab/v1, with real inheritance

Mirrors src/printer/bambu_commands/ as language-agnostic schema docs (not
used for codegen — the Rust structs stay hand-written). envelope.schema.json
is the base; get_version/pushall request+response schemas extend it via
allOf + $ref, which is genuinely closer to real inheritance than anything
else in this project's schema stack: proto and Rust only ever gave us
composition (embed a field holding the base struct), never a mechanism
that reuses a *referenced* schema's constraints automatically.

Verified with the jsonschema Python library, not just written by hand:
all three real examples from the conversation validate correctly, and —
importantly — negative tests confirm the base schema's required fields
and the command consts are actually enforced through the allOf chain, not
just documented. README covers the one real gotcha (additionalProperties:
false on a base schema silently breaks allOf composition, since allOf
validates each sub-schema against the whole instance independently rather
than merging them).
This commit is contained in:
2026-08-28 22:53:59 +00:00
parent 166761479b
commit 42eb0b62d5
9 changed files with 188 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
# bambulab/v1 JSON Schemas
Documentation/validation schemas mirroring `src/printer/bambu_commands/`
not used for codegen (the Rust structs are hand-written, see that module's
doc comment for why), but useful on their own: as language-agnostic
documentation of the wire format, or for validating a captured MQTT
payload without writing Rust to do it.
## The "inheritance" pattern
`envelope.schema.json` is the base — `sequence_id` + `command`, the two
fields every command/response shares. Every other schema here *extends*
it with `allOf` + `$ref`:
```json
{
"allOf": [
{ "$ref": "envelope.schema.json" },
{ "type": "object", "properties": { "command": { "const": "get_version" } } }
]
}
```
This is genuinely closer to real inheritance than anything else in this
project's schema stack (proto, Rust): the instance must satisfy the base
schema *and* the extension schema simultaneously, and — verified, not just
asserted — the base's `required: ["sequence_id", "command"]` is actually
enforced on every schema that extends it, not just copy-pasted as
documentation. Proto and Rust only ever gave us composition (embed a field
holding the base), never something that reuses a *named, referenced*
schema's constraints automatically like this.
**One gotcha if you add a new base-style schema**: don't set
`"additionalProperties": false` on something meant to be `allOf`-extended.
`allOf` validates every sub-schema against the *whole* instance
independently — it doesn't merge object schemas — so a closed base schema
would reject every field the extending schema adds. Leave
`additionalProperties` unset (defaults to allowed) on anything used as a
base; enforce closedness only on the top-level schemas
(`request.schema.json`/`report.schema.json` do this correctly — check
their `additionalProperties: false`).
## Files
- `envelope.schema.json` — the base
- `module_info.schema.json` — nested object used inside `get_version`'s response
- `get_version.request.schema.json` / `get_version.response.schema.json`
- `pushall.request.schema.json` / `pushall.response.schema.json`
- `request.schema.json` / `report.schema.json` — the top-level `{"info": ...}` / `{"pushing": ...}` dispatch, matching `BambuRequest`/`BambuReport`
## Validating something
```python
from referencing import Registry, Resource
from jsonschema import Draft202012Validator
import json, glob, os
resources = []
for path in glob.glob("*.schema.json"):
contents = json.load(open(path))
resources.append((contents["$id"], Resource.from_contents(contents)))
resources.append((os.path.basename(path), Resource.from_contents(contents)))
registry = Registry().with_resources(resources)
schema = json.load(open("request.schema.json"))
Draft202012Validator(schema, registry=registry).validate({"info": {"sequence_id": "0", "command": "get_version"}})
```
+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/envelope.schema.json",
"title": "CommandEnvelope",
"description": "Fields every Bambu MQTT command/response shares. This is the 'base class' — other schemas extend it via allOf + $ref. JSON Schema doesn't have inheritance either, strictly speaking (no 'extends' keyword) — allOf is composition: it says the instance must satisfy THIS schema AND whatever else is listed alongside it. It's the closest thing to real inheritance ergonomics of anything we've used across this whole schema stack (proto, Rust), because $ref + allOf lets you name and reuse a shape without re-declaring its fields, which proto/Rust composition can't quite do (there you still write `common: PrinterReportCommon` by hand on every struct).",
"type": "object",
"properties": {
"sequence_id": { "type": "string" },
"command": { "type": "string" }
},
"required": ["sequence_id", "command"]
}
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/get_version.request.schema.json",
"title": "GetVersionRequest",
"allOf": [
{ "$ref": "envelope.schema.json" },
{
"type": "object",
"properties": {
"command": { "const": "get_version" }
}
}
]
}
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/get_version.response.schema.json",
"title": "GetVersionResponse",
"allOf": [
{ "$ref": "envelope.schema.json" },
{
"type": "object",
"properties": {
"command": { "const": "get_version" },
"result": { "type": "string" },
"reason": { "type": "string" },
"module": {
"type": "array",
"items": { "$ref": "module_info.schema.json" }
}
},
"required": ["module"]
}
]
}
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/module_info.schema.json",
"title": "ModuleInfo",
"type": "object",
"properties": {
"hw_ver": { "type": "string" },
"name": { "type": "string" },
"sn": { "type": "string" },
"sw_ver": { "type": "string" }
},
"required": ["hw_ver", "name", "sn", "sw_ver"]
}
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/pushall.request.schema.json",
"title": "PushallRequest",
"allOf": [
{ "$ref": "envelope.schema.json" },
{
"type": "object",
"properties": {
"command": { "const": "pushall" },
"version": { "type": "integer" },
"push_target": { "type": "integer" }
},
"required": ["version", "push_target"]
}
]
}
@@ -0,0 +1,16 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/pushall.response.schema.json",
"title": "PushallResponse",
"description": "The real pushall/print report (the 'print' category push) has a lot more fields (ams, xcam, tray_exist_bits, ...) — add them under `properties` here the same way you'd add them to src/printer/bambu_commands/pushall.rs. Not required in `required` unless Bambu always sends them.",
"allOf": [
{ "$ref": "envelope.schema.json" },
{
"type": "object",
"properties": {
"result": { "type": "string" },
"reason": { "type": "string" }
}
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/report.schema.json",
"title": "BambuReport",
"description": "Matches src/printer/bambu_commands::BambuReport.",
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"properties": {
"info": { "$ref": "get_version.response.schema.json" },
"pushing": { "$ref": "pushall.response.schema.json" }
},
"additionalProperties": false
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://continuum.internal/schemas/bambulab/v1/request.schema.json",
"title": "BambuRequest",
"description": "Matches src/printer/bambu_commands::BambuRequest — one JSON key naming the category ('info', 'pushing', ...), the value is that category's request schema.",
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"properties": {
"info": { "$ref": "get_version.request.schema.json" },
"pushing": { "$ref": "pushall.request.schema.json" }
},
"additionalProperties": false
}