schemagen silently drops validation-only JSON Schema keywords (minimum, pattern, minLength, uniqueItems, ...), so generated parsers return WeaselJson_OK for schema-invalid documents #70

Open
opened 2026-10-25 12:39:22 +00:00 by weaselbot · 0 comments
Member

Summary

contrib/schemagen/weaseljson_schemagen.py screens a schema node against a hard-coded
list of unsupported keywords (Builder.build_type, lines 314-331). Anything that is not
on that list is silently ignored. That includes validation keywords that decide whether
a document is schema-valid
, e.g. minimum, maximum, exclusiveMinimum,
exclusiveMaximum, multipleOf, minLength, maxLength, pattern, format,
minItems, maxItems, uniqueItems, contains, minContains, minProperties,
maxProperties, propertyNames, unevaluatedProperties.

Generation succeeds with exit code 0 and no diagnostic, the emitted header contains no
trace of the constraint, and the generated RootBuilder therefore returns
WeaselJson_OK for documents that violate the schema.

This contradicts what the tool promises:

  • contrib/schemagen/weaseljson_schemagen.py:8-9:
    "Only the subset of JSON Schema that maps naturally onto C++ structs is supported.
    Anything else is rejected at generation time (no JsonValue fallback)."
  • contrib/schemagen/README.md:29-31:
    "WeaselJson_OK means the document is both valid JSON and schema-valid"
  • contrib/schemagen/README.md:60-68 ("Not supported (rejected at generation time, no
    fallback)") lists const -- a pure validation keyword with no effect on the C++ mapping.
    So the intended design is clearly to reject unsupported validation keywords rather
    than silently drop them.

This is the same class of defect as #55 / #63 (README promises generation-time rejection,
code silently changes what the parser accepts), but it is not a duplicate of any listed
issue: #55/#63 were about additionalProperties and required, not about keywords that
are dropped entirely.

Reproduction

min.schema.json:

{
  "type": "object",
  "additionalProperties": false,
  "required": ["age", "name", "tags"],
  "properties": {
    "age":  {"type": "integer", "minimum": 18, "maximum": 120, "multipleOf": 2},
    "name": {"type": "string", "minLength": 3, "pattern": "^[a-z]+$"},
    "tags": {"type": "array", "items": {"type": "string"}, "minItems": 2, "uniqueItems": true}
  }
}

Generate (succeeds silently, no warning, no trace of the constraints in the output):

$ python3 contrib/schemagen/weaseljson_schemagen.py min.schema.json -o min.h --namespace minschema
$ echo $?
0
$ grep -c -E 'minimum|maximum|multipleOf|minLength|pattern|minItems|uniqueItems' min.h
0

The generated builder then reports success for documents that violate every one of those
constraints:

#include "min.h"

static WeaselJsonStatus feedAll(minschema::RootBuilder &b, std::string doc) {
  std::string copy = doc;  // feed() unescapes in place
  WeaselJsonStatus s = b.feed(copy.data(), (int)copy.size());
  if (s == WeaselJson_AGAIN) s = b.finish();
  return s;
}

int main() {
  const char *docs[] = {
    R"({"age":18,"name":"alice","tags":["a","b"]})",  // schema-valid
    R"({"age":1,"name":"A","tags":["x","x"]})",       // minimum/minLength/pattern/minItems/uniqueItems violated
    R"({"age":1000,"name":"ALICE","tags":[]})",       // maximum/multipleOf/pattern/minItems violated
  };
  for (const char *d : docs) {
    minschema::RootBuilder b;
    WeaselJsonStatus s = feedAll(b, d);
    printf("status=%s  %s\n", s == WeaselJson_OK ? "OK" : (s == WeaselJson_REJECT ? "REJECT" : "OTHER"), d);
    if (s == WeaselJson_OK) {
      auto v = b.take();
      printf("   parsed: age=%lld name=\"%s\" tags=%zu\n", (long long)v.age, v.name.c_str(), v.tags.size());
    }
  }
}

Observed output (built with g++ -std=c++20 -I include -I . min_main.cpp src/lib.cpp):

status=OK  {"age":18,"name":"alice","tags":["a","b"]}
   parsed: age=18 name="alice" tags=2
status=OK  {"age":1,"name":"A","tags":["x","x"]}
   parsed: age=1 name="A" tags=2
status=OK  {"age":1000,"name":"ALICE","tags":[]}
   parsed: age=1000 name="ALICE" tags=0

Expected: either generation fails for these schemas (GenError, like const does), or the
builder rejects the two invalid documents.

For contrast, the same schema with a const sibling on age is rejected at generation
time, which shows the reject-list is the intended mechanism:

$ python3 contrib/schemagen/weaseljson_schemagen.py min_const.schema.json -o min_const.h
error: 'const' is not supported (no natural C++ struct mapping)
$ echo $?
1

Every keyword in the list below was checked with
weaseljson_schemagen.py, and all of them exit 0 silently and leave no trace in the
generated header:

keyword node type used generator exit
minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf integer 0
minLength, maxLength, pattern, format string 0
minItems, maxItems, uniqueItems, contains, minContains, additionalItems array 0
minProperties, maxProperties, propertyNames, unevaluatedProperties object 0

Root cause

Builder.build_type (contrib/schemagen/weaseljson_schemagen.py:314-331) rejects only:

for bad in (
    "allOf", "oneOf", "anyOf", "not", "if", "then", "else",
    "patternProperties", "prefixItems", "const",
    "dependentSchemas", "dependentRequired",
):
    if bad in node:
        raise GenError(f"'{bad}' is not supported (no natural C++ struct mapping)")

There is no allow-list / unknown-keyword check. Any key that is not consulted later in
build_type (type, enum, properties, required, items, $ref,
additionalProperties) is dropped on the floor, because the function only reads the
keys it knows about. Note that if/then/else and const are rejected even though
they have no C++-mapping implication at all -- they are rejected precisely because the
generated parser cannot enforce them -- which is exactly the argument that applies to
minimum/pattern/uniqueItems/... too.

_build_object (:412-462) then emits a field for each property and never records the
constraints, so the generated cbNumberData/cbStringData/cbEndArray code has no way
to know about them.

Impact

Silent schema-validation bypass. A user who generates a parser from a schema and treats
WeaselJson_OK as "the document is valid input" (which is what README.md:29-31
promises) will accept out-of-range numbers, strings that violate pattern/minLength,
arrays with wrong sizes or duplicate elements, objects with too many properties, and so
on, and will happily store and act on that data. Because nothing is printed at generation
time, there is no way for the user to notice the constraint was dropped short of diffing
the generated header against the schema.

Suggested fix

Invert the check in build_type: define the set of keywords the generator actually
understands (plus harmless annotations that are safe to ignore, e.g. title,
description, $comment, default, examples, deprecated, readOnly, writeOnly,
$id, $schema, $anchor, definitions/$defs) and raise GenError for any other key
found in a schema node. At minimum, reject the validation keywords listed above, each of
which is a document-validity constraint that the generated parser cannot enforce. A
regression test can live next to the existing
SchemagenAdditionalPropertiesTest / SchemagenRequiredPropertyTest classes in
contrib/schemagen/test_schemagen.py.

## Summary `contrib/schemagen/weaseljson_schemagen.py` screens a schema node against a hard-coded list of unsupported keywords (`Builder.build_type`, lines 314-331). Anything that is not on that list is silently ignored. That includes **validation keywords that decide whether a document is schema-valid**, e.g. `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `minLength`, `maxLength`, `pattern`, `format`, `minItems`, `maxItems`, `uniqueItems`, `contains`, `minContains`, `minProperties`, `maxProperties`, `propertyNames`, `unevaluatedProperties`. Generation succeeds with exit code 0 and no diagnostic, the emitted header contains no trace of the constraint, and the generated `RootBuilder` therefore returns `WeaselJson_OK` for documents that violate the schema. This contradicts what the tool promises: * `contrib/schemagen/weaseljson_schemagen.py:8-9`: *"Only the subset of JSON Schema that maps naturally onto C++ structs is supported. Anything else is rejected at generation time (no JsonValue fallback)."* * `contrib/schemagen/README.md:29-31`: *"`WeaselJson_OK` means the document is both valid JSON and schema-valid"* * `contrib/schemagen/README.md:60-68` ("Not supported (rejected at generation time, no fallback)") lists `const` -- a pure validation keyword with no effect on the C++ mapping. So the intended design is clearly to *reject* unsupported validation keywords rather than silently drop them. This is the same class of defect as #55 / #63 (README promises generation-time rejection, code silently changes what the parser accepts), but it is not a duplicate of any listed issue: #55/#63 were about `additionalProperties` and `required`, not about keywords that are dropped entirely. ## Reproduction `min.schema.json`: ```json { "type": "object", "additionalProperties": false, "required": ["age", "name", "tags"], "properties": { "age": {"type": "integer", "minimum": 18, "maximum": 120, "multipleOf": 2}, "name": {"type": "string", "minLength": 3, "pattern": "^[a-z]+$"}, "tags": {"type": "array", "items": {"type": "string"}, "minItems": 2, "uniqueItems": true} } } ``` Generate (succeeds silently, no warning, no trace of the constraints in the output): ```sh $ python3 contrib/schemagen/weaseljson_schemagen.py min.schema.json -o min.h --namespace minschema $ echo $? 0 $ grep -c -E 'minimum|maximum|multipleOf|minLength|pattern|minItems|uniqueItems' min.h 0 ``` The generated builder then reports success for documents that violate every one of those constraints: ```cpp #include "min.h" static WeaselJsonStatus feedAll(minschema::RootBuilder &b, std::string doc) { std::string copy = doc; // feed() unescapes in place WeaselJsonStatus s = b.feed(copy.data(), (int)copy.size()); if (s == WeaselJson_AGAIN) s = b.finish(); return s; } int main() { const char *docs[] = { R"({"age":18,"name":"alice","tags":["a","b"]})", // schema-valid R"({"age":1,"name":"A","tags":["x","x"]})", // minimum/minLength/pattern/minItems/uniqueItems violated R"({"age":1000,"name":"ALICE","tags":[]})", // maximum/multipleOf/pattern/minItems violated }; for (const char *d : docs) { minschema::RootBuilder b; WeaselJsonStatus s = feedAll(b, d); printf("status=%s %s\n", s == WeaselJson_OK ? "OK" : (s == WeaselJson_REJECT ? "REJECT" : "OTHER"), d); if (s == WeaselJson_OK) { auto v = b.take(); printf(" parsed: age=%lld name=\"%s\" tags=%zu\n", (long long)v.age, v.name.c_str(), v.tags.size()); } } } ``` Observed output (built with `g++ -std=c++20 -I include -I . min_main.cpp src/lib.cpp`): ``` status=OK {"age":18,"name":"alice","tags":["a","b"]} parsed: age=18 name="alice" tags=2 status=OK {"age":1,"name":"A","tags":["x","x"]} parsed: age=1 name="A" tags=2 status=OK {"age":1000,"name":"ALICE","tags":[]} parsed: age=1000 name="ALICE" tags=0 ``` Expected: either generation fails for these schemas (`GenError`, like `const` does), or the builder rejects the two invalid documents. For contrast, the same schema with a `const` sibling on `age` is rejected at generation time, which shows the reject-list is the intended mechanism: ```sh $ python3 contrib/schemagen/weaseljson_schemagen.py min_const.schema.json -o min_const.h error: 'const' is not supported (no natural C++ struct mapping) $ echo $? 1 ``` Every keyword in the list below was checked with `weaseljson_schemagen.py`, and all of them exit 0 silently and leave no trace in the generated header: | keyword | node type used | generator exit | |---|---|---| | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` | integer | 0 | | `minLength`, `maxLength`, `pattern`, `format` | string | 0 | | `minItems`, `maxItems`, `uniqueItems`, `contains`, `minContains`, `additionalItems` | array | 0 | | `minProperties`, `maxProperties`, `propertyNames`, `unevaluatedProperties` | object | 0 | ## Root cause `Builder.build_type` (`contrib/schemagen/weaseljson_schemagen.py:314-331`) rejects only: ```python for bad in ( "allOf", "oneOf", "anyOf", "not", "if", "then", "else", "patternProperties", "prefixItems", "const", "dependentSchemas", "dependentRequired", ): if bad in node: raise GenError(f"'{bad}' is not supported (no natural C++ struct mapping)") ``` There is no allow-list / unknown-keyword check. Any key that is not consulted later in `build_type` (`type`, `enum`, `properties`, `required`, `items`, `$ref`, `additionalProperties`) is dropped on the floor, because the function only *reads* the keys it knows about. Note that `if`/`then`/`else` and `const` are rejected even though they have no C++-mapping implication at all -- they are rejected precisely because the generated parser cannot enforce them -- which is exactly the argument that applies to `minimum`/`pattern`/`uniqueItems`/... too. `_build_object` (`:412-462`) then emits a field for each property and never records the constraints, so the generated `cbNumberData`/`cbStringData`/`cbEndArray` code has no way to know about them. ## Impact Silent schema-validation bypass. A user who generates a parser from a schema and treats `WeaselJson_OK` as "the document is valid input" (which is what `README.md:29-31` promises) will accept out-of-range numbers, strings that violate `pattern`/`minLength`, arrays with wrong sizes or duplicate elements, objects with too many properties, and so on, and will happily store and act on that data. Because nothing is printed at generation time, there is no way for the user to notice the constraint was dropped short of diffing the generated header against the schema. ## Suggested fix Invert the check in `build_type`: define the set of keywords the generator actually understands (plus harmless annotations that are safe to ignore, e.g. `title`, `description`, `$comment`, `default`, `examples`, `deprecated`, `readOnly`, `writeOnly`, `$id`, `$schema`, `$anchor`, `definitions`/`$defs`) and raise `GenError` for any other key found in a schema node. At minimum, reject the validation keywords listed above, each of which is a document-validity constraint that the generated parser cannot enforce. A regression test can live next to the existing `SchemagenAdditionalPropertiesTest` / `SchemagenRequiredPropertyTest` classes in `contrib/schemagen/test_schemagen.py`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: weaselab/weaseljson#70