Skip to content

feat(cpp-boost-beast): add OAS 3.1 schema validation - #24760

Open
bold84 wants to merge 35 commits into
OpenAPITools:masterfrom
bold84:cpp-boost-beast-oas31
Open

feat(cpp-boost-beast): add OAS 3.1 schema validation#24760
bold84 wants to merge 35 commits into
OpenAPITools:masterfrom
bold84:cpp-boost-beast-oas31

Conversation

@bold84

@bold84 bold84 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

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 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, and deep
enum/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 to types=[string],
float-form count bounds (minItems: 1.0) are dropped (getMinItems()==null),
multi-entry dependentRequired maps get their lists merged, and $dynamicRef
siblings on $ref-carrying schemas are lost. Rather than fight the model, the
generator 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 leaves
OAS 3.0 and parser-retained OAS 3.1 facts untouched.

  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,000 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 validate
    without 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_validation
    and exposed through CamelCase generated headers (Oas31ExactNumber.h,
    Oas31ExactJson.h, Oas31SchemaIr.h, Oas31DeepEqual.h,
    Oas31Validator.h, and Oas31SchemaRegistry.h), so multiple generated
    clients can coexist in one translation unit without type or ODR collisions.

  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); model-qualified public branch accessors
    avoid 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.

  5. SSE and wire support: sseSchemaMode=representation|jsonEventData plus
    per-operation x-sse-event-data-schema opt-in,
    formatAssertionPolicy=annotation|strict, and
    compileWithValidation=true|false (default true; false compiles validation
    out).

  6. Tests (134 focused tests, all passing):
    Oas31IrComplianceTest, CompositionLoweringTest, ModelApiSurfaceTest,
    Oas31ExactRuntimeTest, CppBoostBeastClientApiCodegenTest,
    DependentRequiredParserRetentionTest, and DynamicRefParserRetentionTest.
    Native C++ runtime cases compile with -Wall -Wextra -Werror and exercise
    exact numbers, composition dispatch, public branch accessors, wire paths,
    and two independently generated clients in one translation unit.

  7. Docs & samples: generator page regenerated (all three new CLI options
    documented with defaults); regenerated petstore sample included.

Compatibility

  • OAS 3.0 inputs retain their existing model and API surface while gaining the
    generated validation support files and decode-time validation path.
  • Public model/response/SSE conversion now rejects numeric instances that
    Boost.JSON can only represent as a non-finite value or surrogate; exact schema
    validation still evaluates the original token.
  • Generated CMake now requires C++17 (was C++11).
  • Internal x-oas31-*/x-cpp-* engine channels never appear in generated
    output (regression-tested); the user-facing x-sse-event-data-schema is
    documented in the generator page.

Verification

  • Focused Java/native suite: 134 tests, 0 failures, 0 errors, 0 skips.
  • Full generated-path JSTS slice: 281/281 cases pass.
  • Petstore sample regeneration is byte-stable and its CMake target builds.
  • checkstyle:check and the module verify/forbidden-apis gates pass.
  • Earlier restructuring equivalence gates also passed: Gate A 191/191,
    generated-path 39/39, Wave-1-complete 35/35, and M-probe 50/50.

PR checklist

bold84 added 3 commits August 23, 2026 11:44
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
@bold84
bold84 force-pushed the cpp-boost-beast-oas31 branch from 41628ea to 3081673 Compare August 23, 2026 10:10
@bold84
bold84 force-pushed the cpp-boost-beast-oas31 branch from 3081673 to a6d4582 Compare August 23, 2026 10:22
@bold84
bold84 marked this pull request as ready for review August 24, 2026 22:53

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed 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.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 38 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 15 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .circleci/config.yml Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread .circleci/config.yml Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 18 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 21 files (changes from recent commits).

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/Oas31RawSpecRecovery.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Oas31RawSpecRecovery.java:409">
P1: When `const: null` appears in an `allOf`, this recovered marker is not consulted by composition lowering, so the branch's constraint is omitted from the flattened model. Make composition lowering recognize `hasExplicitConst` and intersect the recovered `constJsonOf` value, including JSON null.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastModelCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CppBoostBeastModelCodegen.java:290">
P1: Nested containers can lose the shared pointer needed by their recursive item. This helper searches any nested pointer while `isCircularReference` only describes the immediate item; strip only when `container.items.dataType` itself starts with `std::shared_ptr<`, or recurse with matching cycle metadata.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

addExtension(parsed, DEFAULT_JSON_EXT, raw.get("default").toString());
}
if (raw.has("const")) {
addExtension(parsed, CONST_PRESENT_EXT, true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When const: null appears in an allOf, this recovered marker is not consulted by composition lowering, so the branch's constraint is omitted from the flattened model. Make composition lowering recognize hasExplicitConst and intersect the recovered constJsonOf value, including JSON null.

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/Oas31RawSpecRecovery.java, line 409:

<comment>When `const: null` appears in an `allOf`, this recovered marker is not consulted by composition lowering, so the branch's constraint is omitted from the flattened model. Make composition lowering recognize `hasExplicitConst` and intersect the recovered `constJsonOf` value, including JSON null.</comment>

<file context>
@@ -402,6 +405,14 @@ private static void recoverSchema(
             addExtension(parsed, DEFAULT_JSON_EXT, raw.get("default").toString());
         }
+        if (raw.has("const")) {
+            addExtension(parsed, CONST_PRESENT_EXT, true);
+            addExtension(parsed, CONST_JSON_EXT, raw.get("const").toString());
+        }
</file context>

* container-level circular-reference flag says the edge is acyclic.
*/
private static void stripNonCyclicContainerItemSharedPtr(CodegenProperty container) {
if (container.isCircularReference || container.dataType == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Nested containers can lose the shared pointer needed by their recursive item. This helper searches any nested pointer while isCircularReference only describes the immediate item; strip only when container.items.dataType itself starts with std::shared_ptr<, or recurse with matching cycle metadata.

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/CppBoostBeastModelCodegen.java, line 290:

<comment>Nested containers can lose the shared pointer needed by their recursive item. This helper searches any nested pointer while `isCircularReference` only describes the immediate item; strip only when `container.items.dataType` itself starts with `std::shared_ptr<`, or recurse with matching cycle metadata.</comment>

<file context>
@@ -282,6 +282,45 @@ private static void stripNonCyclicSharedPtr(CodegenProperty var) {
+     * container-level circular-reference flag says the edge is acyclic.
+     */
+    private static void stripNonCyclicContainerItemSharedPtr(CodegenProperty container) {
+        if (container.isCircularReference || container.dataType == null) {
+            return;
+        }
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant