feat(cpp-boost-beast): add OAS 3.1 schema validation - #24760
Conversation
Generates a compile-time-validating C++ Boost.Beast client for OpenAPI 3.1 documents: densified schema IR, exact numeric lexemes, composition (oneOf/anyOf/allOf) semantics with `CompositionBranchValue` tagged variants, JSON-Schema-2020-12 vocabulary support (`not`, boolean value-schemas, `$dynamicRef`/`$dynamicAnchor`, `dependentRequired`, `contains`, `patternProperties`, `propertyNames`, `if`/`then`/`else`, type arrays, deep enum/const stores), SSE (text/event-stream) modes, multipart encoding metadata, and decode-time validation of every branch. ## What's in this PR 1. **Generator restructuring** (behavior-preserving, output byte-identical): the 9,216-line `CppBoostBeastClientCodegen` monolith is split into `Oas31KeywordScanner`, `Oas31RawSpecRecovery`, `Oas31SchemaIrEmitter` and `Oas31CompositionLowering` (codegen down to ~3,700 lines). Verified by a full battery equivalence run (JSTS 281/281 corpus cases, Gate A 191/191, generated-path 39/39, Wave-1-complete 35/35, M-probe 50/50, sample determinism, `-Werror` compilation of all generated TUs) before and after. 2. **Densified schema IR + evaluator**: every schema/branch/component is flattened into a `SchemaNode` registry (`schema_ir.generated.cpp`) and validated at decode time by the generated `SchemaEvaluator`. Numeric constraints carry their original lexemes (`ExactNumber::parseLexeme` / `setExact`) so values > 2^53, decimals like 0.3 and exponent forms reconstruct exactly — no double-rounding drift. 3. **OAS 3.1 / 2020-12 vocabulary**: type arrays (incl. literal `"null"`), boolean value-schemas, `not` subschemas, deep (array/object) enum/const, `uniqueItems`, `min/maxContains` + `contains`, `dependentRequired` (with raw-literal recovery for the parser's list-merge corruption), `patternProperties`/`propertyNames`, `min/maxProperties`, `$ref` resource identity + `$dynamicRef`/`$dynamicAnchor` scope resolution, annotation keywords per 2020-12 §8.2.6 (`contentEncoding`/`contentMediaType`/ `contentSchema`, `$comment` shape checks). 4. **Composition semantics**: `CompositionBranchValue<N, T>` tagged variants preserve branch identity (dedup, null-collapse, enum-union cases no longer blind-collapse to `std::string`); oneOf exactly-one enforcement; anyOf at-least-one; discriminator-based branch reordering for diagnostics; allOf flat synthetic models with enum intersection; optional-impossible property rejection; fail-closed generation only where a membership-affecting assertion has no generated validator. 5. **SSE and wire support**: `sseSchemaMode=representation|jsonEventData` + `x-sse-event-data-schema` per-operation opt-in, `formatAssertionPolicy= annotation|strict`, `compileWithValidation=true|false` (default true — decode-time validation is the feature; disable to compile it out). 6. **Tests** (120, all passing): test suite split into `Oas31IrComplianceTest` (34) / `CompositionLoweringTest` (46) / `ModelApiSurfaceTest` (33) + `CppBoostBeastTestSupport`; fixtures relocated
41628ea to
3081673
Compare
3081673 to
a6d4582
Compare
There was a problem hiding this comment.
40 issues found across 137 files
Not reviewed (too large): modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastClientCodegen.java (~3,172 lines), modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/CompositionLoweringTest.java (~2,959 lines), modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31SchemaIrEmitter.java (~2,774 lines), modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/Oas31IrComplianceTest.java (~1,998 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:312">
P1: When a 3.1 schema contains multiple composition keywords, this `else-if` records only the first one and drops the other constraint from descriptor-driven validation and lowering. Track each composition keyword independently, then apply all of them to the schema.</violation>
<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:499">
P1: When an allOf branch contains an unsupported assertion, this exemption lets generation proceed without preserving that assertion. Remove the allOf exemption and fail closed, or emit a validator for every unsupported membership assertion before lowering.</violation>
<violation number="3" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:624">
P1: When an allOf contributor closes the object, this discarded value makes the flattened schema treat additional properties as unconstrained. Merge the strictest additionalProperties schema or false value into the synthetic schema.</violation>
<violation number="4" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:683">
P1: For a valid branch such as `{ "type": "number", "exclusiveMinimum": 5 }`, this block skips the bound entirely. Track numeric exclusive bound values separately and intersect them with inclusive bounds before building the synthetic schema.</violation>
<violation number="5" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:890">
P2: The `visited` parameter of intersectPropertySchemas is never consulted or mutated, so the recursive property intersection has no cycle guard. intersectPropertySchemas resolves both operands with ModelUtils.getReferencedSchema and then recurses into nested `getProperties()` (the call at the merged-put site passes `visited` straight through). Unlike resolveAllOfBranch / computeAllOfIntersection, which guard against re-entering a schema name, this path will recurse unboundedly when two intersected object schemas share a cyclic property schema (e.g. a self-referential tree property), risking StackOverflowError during allOf lowering. Either thread the visit-set through (mark/resolve each schema name before descending) or drop the dead parameter; currently the parameter silently implies protection that does not exist.</violation>
<violation number="6" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:898">
P2: When an allOf property uses `$ref` with sibling keywords, this replacement drops the siblings and can produce an incorrect flattened property intersection. Resolve the target while retaining and intersecting the original `$ref` node's sibling assertions.</violation>
<violation number="7" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java:1628">
P2: When a composition has multiple ALWAYS-null branches plus exactly one distinct non-null type (e.g. anyOf [null, null, T]), the nullability is silently dropped. Rule 1 only fires for a 2-branch [T, null] composition; Rule 3 removes all ALWAYS-null branches; then Rule 7 returns early with `deduped.get(0)`, so the null restoration intended by Rule 8 (`hasNull && !nullsAlreadyPreserved`) never runs. The result is a plain `T` C++ type that cannot represent a legitimately-null value, changing decode behavior for an anyOf that allows null. The Rule 8 comment claims "every null surviving to this point must be restored", but Rule 7 returns before that restoration.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/http-client-header.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/http-client-header.mustache:47">
P1: Every normal SSE response reads `streamCancelled` before initializing it, because `HttpResponseData` leaves its scalar members indeterminate. Initialize the status and boolean members in the struct so `streamBody` starts with a valid response state.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/MultipartWireTest.cpp.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/MultipartWireTest.cpp.mustache:81">
P2: This executable never exercises the generated API serializer because it compiles the duplicate defined in this file. Move the serializer into a shared generated helper or link the test against the generated implementation.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/MultipartWireTest.cpp.mustache:141">
P2: When this optional test is built in Release mode, `NDEBUG` disables every validation and CTest can pass on invalid multipart output. Use always-on test failures or explicitly keep assertions enabled for this target.</violation>
<violation number="3" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/MultipartWireTest.cpp.mustache:141">
P2: These assertions do not verify the per-part mapping promised by the test. Parse the multipart body into parts and assert each part's `Content-Disposition`, `Content-Type`, and payload together.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache:81">
P1: When a composed branch is a cyclic model reference (`std::shared_ptr<T>`), variant serialization falls through to `boost::json::value_from(v)`, which does not use the generated model conversion. Add pointer handling to `VariantJsonHelper` and serialize null or `*v` through the model converter.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache:288">
P1: When a composed branch lowers to `std::shared_ptr<T>`, `tryParseBranch` cannot decode it because the fallback only recognizes model objects with `fromJsonValue`. Add a shared-pointer branch that accepts null and delegates non-null values to the pointed-to model converter.</violation>
<violation number="3" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache:531">
P2: With compileWithValidation=false, duplicate-type (x-cpp-has-duplicate-types) variant models always convert branch 0. matchedBranchIndex is forced to 0, so convertMatchedBranch() attempts only branch 0 and throws "Branch 0 conversion failed" when the JSON actually matches a different branch. Select the first parseable branch (like the non-duplicate tryVariantBranches path) instead of hard-coding branch 0.</violation>
<violation number="4" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/model-source.mustache:1501">
P2: For optional const properties, this unconditional assignment emits the key even when the input omitted it. Track const-property presence or emit the inline const only for required properties so optional fields retain omit-versus-present wire semantics.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-header.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-header.mustache:49">
P1: When a response-union operation also declares a non-union response after its successful responses, this filtered loop emits a trailing comma in `std::variant`. Render commas based on the filtered union members, for example by supplying a union-specific last marker from the assembler.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-source.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-source.mustache:445">
P1: When an operation uses a valid relative server URL without a leading slash, the generated request target is malformed (`api/pets`). Resolve relative server URLs to an origin-form path before concatenating the operation path.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-source.mustache:1194">
P1: When a response `oneOf` or `anyOf` contains a `float` branch, variant decoding rejects every valid float value. Add an explicit float conversion path before the model fallback.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastTemplateModelAssembler.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastTemplateModelAssembler.java:282">
P2: When an operation or path explicitly declares `servers: [{ url: "/" }]`, this code treats it as parser-injected and falls through to the parent server. Preserve the source-level distinction between an omitted server list and an explicit `/` override before applying the fallback.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_validator.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_validator.mustache:814">
P2: A valid pure-`$ref` chain longer than 16 hops cannot reach its dynamic anchor, so `$dynamicRef` falls back to the wrong static schema. Replace the fixed hop limit with cycle detection.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_validator.mustache:1032">
P2: When an `if` guard succeeds, its annotations and evaluated-property coverage leak into the enclosing result. Evaluate the guard in a branch and roll it back before evaluating the selected branch.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/validation-types.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/validation-types.mustache:133">
P2: When a `boost::json::value` contains NaN or infinity, `checkJsonType(..., "number")` accepts it as a JSON number. Require `std::isfinite(v.as_double())` for the double case.</violation>
<violation number="2" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/validation-types.mustache:343">
P2: When a schema pattern matches only a substring, `validatePattern` rejects valid instances because `std::regex_match` requires the entire string. Use `std::regex_search` to implement JSON Schema’s unanchored pattern semantics.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31KeywordScanner.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31KeywordScanner.java:155">
P2: When an unsupported version such as `3.10.0` reaches this method, the prefix check misclassifies it as OAS 3.1 and applies the wrong default dialect. Match the OAS minor version exactly before selecting OAS 3.1 behavior.
(Based on your team's feedback about exclusion-based version checks.)</violation>
<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31KeywordScanner.java:529">
P2: A valid schema nested beyond 1024 levels is silently skipped, allowing fail-closed keywords below that point to evade `validateDialectPolicy`. Reject excessive nesting instead of returning as though the subtree was scanned.</violation>
<violation number="3" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31KeywordScanner.java:589">
P2: For a valid `enum: []`, this condition suppresses the keyword occurrence entirely. Record every non-null enum, including an empty list, so the ledger remains exhaustive.</violation>
</file>
<file name="modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/fixtures/composition-fixtures.yaml">
<violation number="1" location="modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/fixtures/composition-fixtures.yaml:452">
P3: The OneOfStringStringEnum description says to type-erase to boost::json::value, but the tests for this same fixture assert the opposite: the generator produces std::variant<CompositionBranchValue<0, std::string>, CompositionBranchValue<1, std::string>>, and oneOfStringStringEnumViaGateFixtures explicitly fails if the model is boost::json::value. Update the description to state the actual branch-preserving behavior so it does not misdocument the expected output.</violation>
<violation number="2" location="modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/fixtures/composition-fixtures.yaml:754">
P2: The AnyOfEnumUnion description documents an expected behavior that is both wrong and semantically unsound, and it contradicts the tests written for this fixture. Collapsing [enum red] ∪ [enum blue] to std::string would accept any string (e.g. "green"), which violates the schema, so the "collapse is sound" reasoning is false (unlike AnyOfStringStringEnum, whose plain string branch genuinely accepts all strings). The generator in fact produces a CompositionBranchValue variant that preserves per-branch enum validators, as the tests assert. Fix the description so it does not instruct a sound collapse to std::string.</violation>
<violation number="3" location="modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/fixtures/composition-fixtures.yaml:771">
P3: The AllNullAnyOf description says it should produce boost::json::value, but the tests for this fixture assert it must be CompositionBranchValue<0, std::nullptr_t> to preserve null cardinality. Update the description to match the actual branch-preserving lowering.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/NullableField.h.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/NullableField.h.mustache:63">
P2: For an optional nullable property with a schema default, passing `getField()` back to the generated setter does not mark the field present because the wrapper preserves `missing_ == true`; serialization therefore still omits it. Add a distinct present-value/null promotion in the setter path while preserving missing state for default initialization.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/CMakeLists.txt.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/CMakeLists.txt.mustache:104">
P2: When the generated client is consumed through `add_subdirectory`, sibling targets may not be able to resolve the newly PUBLIC `Boost::json` imported target. Promote the Boost imported targets to `IMPORTED_GLOBAL` (or avoid exposing a directory-scoped target) before publishing this interface.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_json.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/oas31_exact_json.mustache:307">
P2: When `payload` contains malformed numeric syntax such as `1.2.3`, `captureAndSanitizeNumbers` can turn it into valid JSON and this fallback returns it. Retry the sanitized payload only after proving the original error was numeric-representation overflow; otherwise propagate the original parse error.</violation>
</file>
<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/CppBoostBeastTestSupport.java">
<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/CppBoostBeastTestSupport.java:64">
P2: `extractMethod` and `countOccurrences` here duplicate existing helpers: `TestUtils.countOccurrences` (TestUtils.java:188) and the private `extractMethod`/`countOccurrences` that `CppBoostBeastClientApiCodegenTest.java` still defines at lines 311-329. The new copies drift in parallel with the old ones; reuse the shared helpers (or migrate the API test to this class) instead of adding a third implementation.</violation>
</file>
<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31RawSpecRecovery.java">
<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31RawSpecRecovery.java:98">
P2: When a tuple schema omits `type: array`, this guard prevents recovery of its `prefixItems`. Recover based on the presence of raw `prefixItems`, because array keywords remain valid and meaningful when the schema type is omitted.</violation>
</file>
<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/DependentRequiredParserRetentionTest.java">
<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/DependentRequiredParserRetentionTest.java:61">
P2: This regression pin never detects the parser fix it is meant to catch, so it passes whether or not swagger-parser merges the dependentRequired lists. The fixture keys are "foo\nbar"->["req\rA"] and "foo'bar"->["req'B"], and the test accumulates seenCr from one list and seenQuote from the other. When the parser is fixed (lists stay separate), the first still sets seenCr and the second sets seenQuote, so `assertTrue(seenCr && seenQuote)` passes; the MERGED-union corruption is not distinguished from correct behavior. Assert within one list instead that the first trigger's list also contains the second trigger's member (and/or that both entries share the same list object), per the corruption signals in the comment.</violation>
</file>
<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/DynamicRefParserRetentionTest.java">
<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/DynamicRefParserRetentionTest.java:28">
P3: The test claims to "prove what swagger-parser retains for $dynamicRef / $dynamicAnchor / $anchor / embedded $id", and its spec defines $defs.foo with $dynamicAnchor and $defs.bar with $anchor/$id, but none of those are ever asserted. Only items.get$dynamicRef() and the x-oas31-resource extension are checked. If swagger-parser 2.1.47 silently drops $dynamicAnchor/$anchor/$id, this test still passes, so the retention property it advertises (and which Oas31SchemaIrEmitter relies on via get$dynamicAnchor) is untested and the javadoc overstates coverage.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-operation-source.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/cpp-boost-beast-client/api-operation-source.mustache:199">
P2: In `sseSchemaMode=jsonEventData`, the generated SSE callback returns `true` for a `[DONE]` event, which tells `SseEventFramer` to keep streaming (`if (!m_onEvent(event)) m_cancelled = true;` in http-client-impl-source.mustache). The README documents `[DONE]` as a terminator ("consumed as terminator before decoding") and that returning `false` cancels the stream cooperatively, so after `[DONE]` the client should stop reading. As written, a server that sends `[DONE]` but keeps the connection open leaves the client waiting indefinitely for further events. Return `false` for `[DONE]` to terminate the stream.</violation>
</file>
<file name="modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/nullable-object-regression.yaml">
<violation number="1" location="modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/nullable-object-regression.yaml:3">
P3: This new spec is never wired to a test: no test references the 3_1/cpp-boost-beast-client/nullable-object-regression.yaml path, and none of the cpp-boost-beast tests load resources by directory listing. The only test exercising a nullable object (generatesOas30NullableObject) reads the 3_0 copy instead. Either add a test that consumes this fixture or remove it, so the regression it documents is actually verified.</violation>
</file>
<file name="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/ModelApiSurfaceTest.java">
<violation number="1" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/ModelApiSurfaceTest.java:303">
P3: These comments claim the generator is broken ('CURRENT BROKEN BEHAVIOUR ... emits only an IsSet flag' / 'does NOT produce a status-aware response union') yet the very next assertions require the fixed behavior (NullableField, response-union struct). Update or drop the stale comments so they match what the tests actually verify.</violation>
<violation number="2" location="modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/ModelApiSurfaceTest.java:1332">
P2: Files.createTempDirectory(Path.of("target"), ...) throws NoSuchFileException when no 'target' directory exists. The other test here (callbacksAndResponseLinksArePreservedFromParsedModel) explicitly creates target first; these two sites rely on it existing by chance. Create the directory (or use the system temp dir) before creating the temp subdir.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| // assertions don't change match count. | ||
| if ("not".equals(unsupported)) { | ||
| // `not` always fails generation regardless of keyword | ||
| } else if ("allOf".equals(desc.getKeyword())) { |
There was a problem hiding this comment.
P1: When an allOf branch contains an unsupported assertion, this exemption lets generation proceed without preserving that assertion. Remove the allOf exemption and fail closed, or emit a validator for every unsupported membership assertion before lowering.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java, line 499:
<comment>When an allOf branch contains an unsupported assertion, this exemption lets generation proceed without preserving that assertion. Remove the allOf exemption and fail closed, or emit a validator for every unsupported membership assertion before lowering.</comment>
<file context>
@@ -0,0 +1,1645 @@
+ // assertions don't change match count.
+ if ("not".equals(unsupported)) {
+ // `not` always fails generation regardless of keyword
+ } else if ("allOf".equals(desc.getKeyword())) {
+ continue; // non-not unsupported assertions exempted for allOf
+ }
</file context>
| List<Schema> branchSchemas = null; | ||
| String keyword = null; | ||
|
|
||
| if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) { |
There was a problem hiding this comment.
P1: When a 3.1 schema contains multiple composition keywords, this else-if records only the first one and drops the other constraint from descriptor-driven validation and lowering. Track each composition keyword independently, then apply all of them to the schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java, line 312:
<comment>When a 3.1 schema contains multiple composition keywords, this `else-if` records only the first one and drops the other constraint from descriptor-driven validation and lowering. Track each composition keyword independently, then apply all of them to the schema.</comment>
<file context>
@@ -0,0 +1,1645 @@
+ List<Schema> branchSchemas = null;
+ String keyword = null;
+
+ if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
+ branchSchemas = schema.getOneOf();
+ keyword = "oneOf";
</file context>
| boost::beast::http::status status; | ||
| std::map<std::string, std::string> headers; | ||
| std::string body; | ||
| bool isEventStream = false; | ||
| bool streamCancelled = false; |
There was a problem hiding this comment.
P1: Every normal SSE response reads streamCancelled before initializing it, because HttpResponseData leaves its scalar members indeterminate. Initialize the status and boolean members in the struct so streamBody starts with a valid response state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/cpp-boost-beast-client/http-client-header.mustache, line 47:
<comment>Every normal SSE response reads `streamCancelled` before initializing it, because `HttpResponseData` leaves its scalar members indeterminate. Initialize the status and boolean members in the struct so `streamBody` starts with a valid response state.</comment>
<file context>
@@ -1,17 +1,57 @@
+
+/// Rich HTTP response containing status, response headers, and body.
+struct HttpResponseData {
+ boost::beast::http::status status;
+ std::map<std::string, std::string> headers;
+ std::string body;
</file context>
| boost::beast::http::status status; | |
| std::map<std::string, std::string> headers; | |
| std::string body; | |
| bool isEventStream = false; | |
| bool streamCancelled = false; | |
| boost::beast::http::status status = boost::beast::http::status::unknown; | |
| std::map<std::string, std::string> headers; | |
| std::string body; | |
| bool isEventStream = false; | |
| bool streamCancelled = false; |
| } | ||
|
|
||
| // Intersect numeric bounds: minimum/maximum take tighter range | ||
| if (resolvedBranch.getMinimum() != null) { |
There was a problem hiding this comment.
P1: For a valid branch such as { "type": "number", "exclusiveMinimum": 5 }, this block skips the bound entirely. Track numeric exclusive bound values separately and intersect them with inclusive bounds before building the synthetic schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java, line 683:
<comment>For a valid branch such as `{ "type": "number", "exclusiveMinimum": 5 }`, this block skips the bound entirely. Track numeric exclusive bound values separately and intersect them with inclusive bounds before building the synthetic schema.</comment>
<file context>
@@ -0,0 +1,1645 @@
+ }
+
+ // Intersect numeric bounds: minimum/maximum take tighter range
+ if (resolvedBranch.getMinimum() != null) {
+ hasRootScalarConstraints = true;
+ BigDecimal branchMin = resolvedBranch.getMinimum();
</file context>
| // result is closed. If multiple contributors constrain the | ||
| // additional properties schema, use the stricter intersection. | ||
| // Preserve the strictest additionalProperties constraint available. | ||
| Object branchAddProps = resolvedBranch.getAdditionalProperties(); |
There was a problem hiding this comment.
P1: When an allOf contributor closes the object, this discarded value makes the flattened schema treat additional properties as unconstrained. Merge the strictest additionalProperties schema or false value into the synthetic schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31CompositionLowering.java, line 624:
<comment>When an allOf contributor closes the object, this discarded value makes the flattened schema treat additional properties as unconstrained. Merge the strictest additionalProperties schema or false value into the synthetic schema.</comment>
<file context>
@@ -0,0 +1,1645 @@
+ // result is closed. If multiple contributors constrain the
+ // additional properties schema, use the stricter intersection.
+ // Preserve the strictest additionalProperties constraint available.
+ Object branchAddProps = resolvedBranch.getAdditionalProperties();
+ if (branchAddProps != null) {
+ // Track the constraint — but we don't currently produce a
</file context>
| AllNullAnyOf: | ||
| description: > | ||
| anyOf over two null branches. The only valid value is null. | ||
| Should produce boost::json::value since all branches are type void. |
There was a problem hiding this comment.
P3: The AllNullAnyOf description says it should produce boost::json::value, but the tests for this fixture assert it must be CompositionBranchValue<0, std::nullptr_t> to preserve null cardinality. Update the description to match the actual branch-preserving lowering.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/fixtures/composition-fixtures.yaml, line 771:
<comment>The AllNullAnyOf description says it should produce boost::json::value, but the tests for this fixture assert it must be CompositionBranchValue<0, std::nullptr_t> to preserve null cardinality. Update the description to match the actual branch-preserving lowering.</comment>
<file context>
@@ -0,0 +1,902 @@
+ AllNullAnyOf:
+ description: >
+ anyOf over two null branches. The only valid value is null.
+ Should produce boost::json::value since all branches are type void.
+ anyOf:
+ - type: "null"
</file context>
| Should produce boost::json::value since all branches are type void. | |
| + Should lower as CompositionBranchValue<0, std::nullptr_t> to preserve | |
| + null branch identity. |
| enum members match both branches (invalid oneOf). Emit boost::json::value | ||
| instead of a false exclusive typed union. | ||
| oneOf: |
There was a problem hiding this comment.
P3: The OneOfStringStringEnum description says to type-erase to boost::json::value, but the tests for this same fixture assert the opposite: the generator produces std::variant<CompositionBranchValue<0, std::string>, CompositionBranchValue<1, std::string>>, and oneOfStringStringEnumViaGateFixtures explicitly fails if the model is boost::json::value. Update the description to state the actual branch-preserving behavior so it does not misdocument the expected output.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/fixtures/composition-fixtures.yaml, line 452:
<comment>The OneOfStringStringEnum description says to type-erase to boost::json::value, but the tests for this same fixture assert the opposite: the generator produces std::variant<CompositionBranchValue<0, std::string>, CompositionBranchValue<1, std::string>>, and oneOfStringStringEnumViaGateFixtures explicitly fails if the model is boost::json::value. Update the description to state the actual branch-preserving behavior so it does not misdocument the expected output.</comment>
<file context>
@@ -0,0 +1,902 @@
+ oneOf over a plain string and a string enum. The generator MUST NOT
+ apply the anyOf-only string+enum collapse. std::variant<std::string,
+ std::string> is invalid C++, and collapsing to std::string hides that
+ enum members match both branches (invalid oneOf). Emit boost::json::value
+ instead of a false exclusive typed union.
+ oneOf:
</file context>
| enum members match both branches (invalid oneOf). Emit boost::json::value | |
| instead of a false exclusive typed union. | |
| oneOf: | |
| + enum members match both branches (invalid oneOf). Lower as a | |
| + CompositionBranchValue variant to preserve branch identity instead of | |
| + type-erasing to boost::json::value. |
| + " \"items\": {\"$dynamicRef\": \"#items\"},\n" | ||
| + " \"x-oas31-resource\": 7,\n" | ||
| + " \"$defs\": {\n" | ||
| + " \"foo\": {\"$dynamicAnchor\": \"items\", \"type\": \"string\"},\n" |
There was a problem hiding this comment.
P3: The test claims to "prove what swagger-parser retains for $dynamicRef / $dynamicAnchor / $anchor / embedded $id", and its spec defines $defs.foo with $dynamicAnchor and $defs.bar with $anchor/$id, but none of those are ever asserted. Only items.get$dynamicRef() and the x-oas31-resource extension are checked. If swagger-parser 2.1.47 silently drops $dynamicAnchor/$anchor/$id, this test still passes, so the retention property it advertises (and which Oas31SchemaIrEmitter relies on via get$dynamicAnchor) is untested and the javadoc overstates coverage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/DynamicRefParserRetentionTest.java, line 28:
<comment>The test claims to "prove what swagger-parser retains for $dynamicRef / $dynamicAnchor / $anchor / embedded $id", and its spec defines $defs.foo with $dynamicAnchor and $defs.bar with $anchor/$id, but none of those are ever asserted. Only items.get$dynamicRef() and the x-oas31-resource extension are checked. If swagger-parser 2.1.47 silently drops $dynamicAnchor/$anchor/$id, this test still passes, so the retention property it advertises (and which Oas31SchemaIrEmitter relies on via get$dynamicAnchor) is untested and the javadoc overstates coverage.</comment>
<file context>
@@ -0,0 +1,93 @@
+ + " \"items\": {\"$dynamicRef\": \"#items\"},\n"
+ + " \"x-oas31-resource\": 7,\n"
+ + " \"$defs\": {\n"
+ + " \"foo\": {\"$dynamicAnchor\": \"items\", \"type\": \"string\"},\n"
+ + " \"bar\": {\"$anchor\": \"plain\", \"$id\": \"urn:embedded\", \"type\": \"number\"}\n"
+ + " }\n"
</file context>
| @@ -0,0 +1,25 @@ | |||
| openapi: 3.0.3 | |||
| info: | |||
| title: Nullable object regression | |||
There was a problem hiding this comment.
P3: This new spec is never wired to a test: no test references the 3_1/cpp-boost-beast-client/nullable-object-regression.yaml path, and none of the cpp-boost-beast tests load resources by directory listing. The only test exercising a nullable object (generatesOas30NullableObject) reads the 3_0 copy instead. Either add a test that consumes this fixture or remove it, so the regression it documents is actually verified.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/resources/3_1/cpp-boost-beast-client/nullable-object-regression.yaml, line 3:
<comment>This new spec is never wired to a test: no test references the 3_1/cpp-boost-beast-client/nullable-object-regression.yaml path, and none of the cpp-boost-beast tests load resources by directory listing. The only test exercising a nullable object (generatesOas30NullableObject) reads the 3_0 copy instead. Either add a test that consumes this fixture or remove it, so the regression it documents is actually verified.</comment>
<file context>
@@ -0,0 +1,25 @@
+openapi: 3.0.3
+info:
+ title: Nullable object regression
+ version: 1.0.0
+paths:
</file context>
| public void generatesOptionalNullableTriState() throws IOException { | ||
| // Optional nullable property must preserve missing, null, and value. | ||
| // The tri-state requires a Nullable<T>-like field wrapper, not just an IsSet bool. | ||
| // CURRENT BROKEN BEHAVIOUR: the generator emits only an IsSet flag, which cannot |
There was a problem hiding this comment.
P3: These comments claim the generator is broken ('CURRENT BROKEN BEHAVIOUR ... emits only an IsSet flag' / 'does NOT produce a status-aware response union') yet the very next assertions require the fixed behavior (NullableField, response-union struct). Update or drop the stale comments so they match what the tests actually verify.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/cppboostbeast/ModelApiSurfaceTest.java, line 303:
<comment>These comments claim the generator is broken ('CURRENT BROKEN BEHAVIOUR ... emits only an IsSet flag' / 'does NOT produce a status-aware response union') yet the very next assertions require the fixed behavior (NullableField, response-union struct). Update or drop the stale comments so they match what the tests actually verify.</comment>
<file context>
@@ -0,0 +1,1624 @@
+ public void generatesOptionalNullableTriState() throws IOException {
+ // Optional nullable property must preserve missing, null, and value.
+ // The tri-state requires a Nullable<T>-like field wrapper, not just an IsSet bool.
+ // CURRENT BROKEN BEHAVIOUR: the generator emits only an IsSet flag, which cannot
+ // distinguish missing from explicit null. This test locks the tri-state gap.
+ File output = java.nio.file.Files.createTempDirectory("cpp-boost-beast-tri-state").toFile();
</file context>
cpp-boost-beast-client: OAS 3.1 schema validation support
Generates C++ Boost.Beast clients with decode-time validation for OpenAPI 3.1
documents: a densified schema IR, exact numeric lexemes, composition
(
oneOf/anyOf/allOf) semantics withCompositionBranchValuetaggedvariants, JSON Schema 2020-12 vocabulary support (
not, boolean value-schemas,$dynamicRef/$dynamicAnchor,dependentRequired,contains,patternProperties,propertyNames,if/then/else, type arrays, and deepenum/const values), SSE modes, and multipart encoding metadata.
This is a follow-up to #24335, which introduced the Boost.Beast client
generator and its initial OpenAPI 3.1 support. It supersedes the
composition-only draft #24387; that work is incorporated here. Discussion of
the three proposed generator options is tracked in #24761.
What's in this PR
Motivation / parser rationale
OpenAPI 3.1 documents cannot be generated faithfully by the upstream
swagger-parser 2.1.x model layer alone:
type: [...]arrays (incl. a literal"null"member) are lowered away,enum: []degrades totypes=[string],float-form count bounds (
minItems: 1.0) are dropped (getMinItems()==null),multi-entry
dependentRequiredmaps get their lists merged, and$dynamicRefsiblings on
$ref-carrying schemas are lost. Rather than fight the model, thegenerator recovers the pristine facts from the raw spec text and re-injects
them as extension-marked channels that only fire when the parser has already
dropped information (see
Oas31RawSpecRecovery). Recovery therefore leavesOAS 3.0 and parser-retained OAS 3.1 facts untouched.
Generator restructuring (behavior-preserving, output byte-identical):
the 9,216-line
CppBoostBeastClientCodegenmonolith is split intoOas31KeywordScanner,Oas31RawSpecRecovery,Oas31SchemaIrEmitterandOas31CompositionLowering(codegen down to ~3,000 lines). Verified by afull battery equivalence run (JSTS 281/281 corpus cases, Gate A 191/191,
generated-path 39/39, Wave-1-complete 35/35, M-probe 50/50, sample
determinism,
-Werrorcompilation of all generated TUs) before and after.Densified schema IR + evaluator: every schema/branch/component is
flattened into a
SchemaNoderegistry (schema_ir.generated.cpp) andvalidated at decode time by the generated
SchemaEvaluator. Numericconstraints carry their original lexemes (
ExactNumber::parseLexeme/setExact) so values > 2^53, decimals like 0.3 and exponent forms validatewithout double-rounding drift. If Boost.JSON cannot represent an instance
number, validation still uses its exact token and public conversion rejects
the payload instead of exposing a surrogate or non-JSON value. The runtime
is rendered per client into
<modelNamespace>::detail::schema_validationand exposed through CamelCase generated headers (
Oas31ExactNumber.h,Oas31ExactJson.h,Oas31SchemaIr.h,Oas31DeepEqual.h,Oas31Validator.h, andOas31SchemaRegistry.h), so multiple generatedclients can coexist in one translation unit without type or ODR collisions.
OAS 3.1 / 2020-12 vocabulary: type arrays (incl. literal
"null"),boolean value-schemas,
notsubschemas, deep (array/object) enum/const,uniqueItems,min/maxContains+contains,dependentRequired(withraw-literal recovery for the parser's list-merge corruption),
patternProperties/propertyNames,min/maxProperties,$refresourceidentity +
$dynamicRef/$dynamicAnchorscope resolution, annotationkeywords per 2020-12 §8.2.6 (
contentEncoding/contentMediaType/contentSchema,$commentshape checks).Composition semantics:
CompositionBranchValue<N, T>tagged variantspreserve branch identity (dedup, null-collapse, enum-union cases no longer
blind-collapse to
std::string); model-qualified public branch accessorsavoid cross-model symbol collisions; oneOf enforces exactly one match and
anyOf at least one; discriminator mappings reorder diagnostics; allOf builds
flat synthetic models with JSON-value enum/const intersection; optional-
impossible properties are rejected; unsupported membership assertions fail
generation closed.
SSE and wire support:
sseSchemaMode=representation|jsonEventDataplusper-operation
x-sse-event-data-schemaopt-in,formatAssertionPolicy=annotation|strict, andcompileWithValidation=true|false(default true; false compiles validationout).
Tests (134 focused tests, all passing):
Oas31IrComplianceTest,CompositionLoweringTest,ModelApiSurfaceTest,Oas31ExactRuntimeTest,CppBoostBeastClientApiCodegenTest,DependentRequiredParserRetentionTest, andDynamicRefParserRetentionTest.Native C++ runtime cases compile with
-Wall -Wextra -Werrorand exerciseexact numbers, composition dispatch, public branch accessors, wire paths,
and two independently generated clients in one translation unit.
Docs & samples: generator page regenerated (all three new CLI options
documented with defaults); regenerated petstore sample included.
Compatibility
generated validation support files and decode-time validation path.
Boost.JSON can only represent as a non-finite value or surrogate; exact schema
validation still evaluates the original token.
x-oas31-*/x-cpp-*engine channels never appear in generatedoutput (regression-tested); the user-facing
x-sse-event-data-schemaisdocumented in the generator page.
Verification
checkstyle:checkand the moduleverify/forbidden-apis gates pass.generated-path 39/39, Wave-1-complete 35/35, and M-probe 50/50.
PR checklist
master.x-sse-event-data-schema.forbiddenapis/checkstyle gates green.