schema_registry: broker aborts on a JSON schema compatibility check when a required property has a boolean subschema
Version & Environment
Redpanda version (rpk version): v26.1.17 (git ref 3501d0e25f66957023e0d9c35a914309c0d5b41b)
Reproduced on v26.1.17 and v26.2.2 (currently tagged latest). Not reproducible on v26.1.10 or v25.3.17.
All testing was against the official container images (redpandadata/redpanda:<tag>) — single node, --smp 1 --memory 1G, on macOS/arm64 under Docker Desktop. Not a build from source, and not a Cloud or Serverless cluster. No Kafka client libraries were involved; every call was plain HTTP against the schema registry REST API.
What went wrong?
A single schema registration request terminates the broker process.
If a subject's registered version 1 contains a property whose schema is the boolean true, and that property is also listed in required, then registering any new schema version on that subject aborts the broker. The client doesn't get an error response — the connection simply closes:
ERROR [shard 0:main] assert - Assert failure:
(rapidjson/document.h:1359) 'IsObject()' Rapidjson
ERROR [shard 0:main] assert - Backtrace:
... src/v/pandaproxy/schema_registry/sharded_store.cc:972
... seastar::internal::coroutine_traits_base<pandaproxy::schema_registry::compatibility_result>
Recorded crash reason to crash file on shard 0 (vassert)curl reports exit 52 (empty reply). The container exits 133. On a single-node cluster the registry does not come back.
Why it happens. is_object_required_superset in src/v/pandaproxy/schema_registry/json.cc walks the older schema's required list and asks each named property whether it carries a default:
std::ranges::for_each(older_req, [&](const json::Value& o) {
auto it = older_props.FindMember(o);
bool has_default = it != older_props.MemberEnd()
&& it->value.HasMember("default");it->value is whatever sits in properties under that name. For "payload": true that's a boolean, and HasMember → FindMember asserts IsObject(). Since Redpanda maps RAPIDJSON_ASSERT to vassert (src/v/json/_include_first.h), which is enabled in release builds, the assertion aborts the process rather than being compiled out.
Worth noting that the file already knows booleans are valid schemas and normalizes them in two places — get_schema and get_object_or_empty, both carrying the comment "in >= draft6 'true/false' is a valid schema and means {}/{"not":{}}". This one call site reaches into properties with a raw FindMember and skips that normalization.
Two things make it more than a crash. First, the blast radius: the schema registry is cluster-wide, so one subject takes down schema resolution for every client, not just the publisher. Second, the affected subject can never be evolved again — registered versions are immutable, so the true in version 1 can't be edited out, and every subsequent attempt re-triggers the abort. The only escape is setting the subject's compatibility to NONE, registering a replacement version so no comparison runs, then restoring the compatibility level.
This is easy to hit unintentionally. true is what schema generators emit for a field with no fixed shape — Go's json.RawMessage under invopop/jsonschema produces exactly this, and marks it required unless the field has omitempty. Nothing about the resulting schema looks unusual, and it validates fine everywhere else.
What should have happened instead?
The comparison should complete and return an HTTP response. A boolean subschema has no default, so has_default should be false.
In the reproducer below, the new version adds one optional property, which is compatible under FULL. The expected result is 200 and a new version id — which is what v26.1.10 and v25.3.17 return.
How to reproduce the issue?
Start a single node:
docker run -d --name rp -p 8081:8081 redpandadata/redpanda:v26.2.2 \
redpanda start --overprovisioned --smp 1 --memory 1G --reserve-memory 0M \
--node-id 0 --check=false --kafka-addr 0.0.0.0:9092 \
--advertise-kafka-addr 127.0.0.1:9092 --schema-registry-addr 0.0.0.0:8081Register a schema with a required property whose subschema is true:
curl -X PUT http://localhost:8081/config/repro-value \
-H 'Content-Type: application/json' -d '{"compatibility":"FULL"}'
curl -X POST http://localhost:8081/subjects/repro-value/versions \
-H 'Content-Type: application/vnd.schemaregistry.v1+json' \
-d '{"schemaType":"JSON","schema":"{\"type\":\"object\",\"properties\":{\"payload\":true},\"required\":[\"payload\"]}"}'
# => {"id":1,"version":1,...}Register a second version that adds one optional property:
curl -X POST http://localhost:8081/subjects/repro-value/versions \
-H 'Content-Type: application/vnd.schemaregistry.v1+json' \
-d '{"schemaType":"JSON","schema":"{\"type\":\"object\",\"properties\":{\"payload\":true,\"extra\":{\"type\":\"string\"}},\"required\":[\"payload\"]}"}'
# => curl: (52) Empty reply from serverdocker ps -a now shows Exited (133), and docker logs rp contains the assertion above. BACKWARD behaves identically; any mode other than NONE should be affected, since the crash is in the comparison itself.
Suggested fix
The minimal change is a type check before the member lookup:
std::ranges::for_each(older_req, [&](const json::Value& o) {
auto it = older_props.FindMember(o);
bool has_default = it != older_props.MemberEnd()
- && it->value.HasMember("default");
+ && it->value.IsObject()
+ && it->value.HasMember("default");This is also the semantically correct answer rather than just a crash guard: a boolean subschema genuinely has no default, so the property stays subject to the normal required rules and the comparison reports required_attribute_added (or nothing) as appropriate.
If you'd rather keep the boolean handling in one place, routing the lookup through the existing get_object_or_empty / get_schema helpers would match the convention used elsewhere in the file, at the cost of materializing a canonical schema object for a check that only needs one member.
A regression test would want a schema pair like the one above — {"properties": {"x": true}, "required": ["x"]} evolving to add an unrelated optional property. As far as I can tell the JSON compatibility corpus has no case with a boolean subschema for a required property, which is plausibly why this wasn't caught.
Two smaller suggestions, take or leave:
- A grep for
HasMemberacrossjson.ccreturns only two call sites — this one and one inis_positive_combinator_supersetwhere the value is already known to be an object. So the audit surface here is small, and the helpers above mean most of the file is already correct by construction. - Since a valid schema document can abort the broker, it may be worth deciding whether
vassertis the right severity for rapidjson type assertions on client-supplied documents, independent of this particular fix.
Additional information
Regression range. Not reproducible on v26.1.10, reproducible on v26.1.17. That's consistent with #30525 ("schema_registry: enforce strict validation for missing required attributes", merged to dev 2026-05-20) and its v26.1.x backport #30563 (merged 2026-06-22, milestone v26.1.11). Individual patch releases between v26.1.11 and v26.1.16 were not tested. The v25.2.x (#30559) and v25.3.x (#30562) backports were closed unmerged, which matches v25.3.17 being unaffected.
Isolating the trigger. On v26.1.17, adding the same optional property each time:
| Version 1 contains | Result |
|---|---|
"payload": true, listed in required |
broker aborts |
"payload": {}, listed in required |
200, broker healthy |
"payload": true, not in required |
200, broker healthy |
"payload": {"type":"boolean"}, in required |
200, broker healthy |
Both halves are needed: the boolean subschema, and the property appearing in required. Note that {} and true are equivalent schemas, so the difference between rows one and two is purely which spelling the producer happened to emit.
On fix availability. The code path is unchanged on dev as of this report, and the affected releases include the newest (v26.2.2). For clusters on Redpanda Cloud Serverless, where the version isn't selectable, there's currently no version that both exists and avoids this.
Source: redpanda-data/redpanda