Skip to content

Feat/normalizer default propagation#23821

Draft
Picazsoo wants to merge 9 commits into
OpenAPITools:masterfrom
Picazsoo:feat/normalizer-default-propagation
Draft

Feat/normalizer default propagation#23821
Picazsoo wants to merge 9 commits into
OpenAPITools:masterfrom
Picazsoo:feat/normalizer-default-propagation

Conversation

@Picazsoo
Copy link
Copy Markdown
Contributor

@Picazsoo Picazsoo commented May 19, 2026

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Introduces a configurable DefaultResolutionStrategy to resolve default values in allOf schemas and makes $ref defaults reliably visible to generators by pre-resolving refs in DefaultCodegen.fromProperty. Also adds an opt-in normalizer rule RESOLVE_SCHEMA_DEFAULTS to write resolved defaults back to schemas.

  • New Features

    • ModelUtils.resolveDefault() with strategies: LAST_WINS, NEAREST_WINS, ROOT_WINS, STRICT (see docs/default-resolution-strategy.md).
    • Normalizer flag RESOLVE_SCHEMA_DEFAULTS=<strategy> resolves and applies default to component/property schemas; skips pure $ref to keep OAS validity.
    • Lint recommendations for allOf conflicts and shadowed defaults in OpenApiSchemaValidations.
  • Refactors

    • $ref default propagation moved to DefaultCodegen.fromProperty (deep $ref traversal); removed redundant ref resolution in AbstractJavaCodegen/AbstractKotlinCodegen; restored follow-ref in AbstractPythonCodegen.toDefaultValue(Schema) for direct calls.
    • C++: ensure model members are default-constructed (CppHttplibServerCodegen, CppTinyClientCodegen, CppTizenClientCodegen).
    • Added tests and refreshed samples; generated C# docs now display enum defaults.

Written for commit 6177051. Summary will update on new commits. Review in cubic

Picazsoo and others added 9 commits May 19, 2026 09:51
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…LTS rule

Change 1 (always-on): when a property schema is a pure ref with no
direct default, copy the default value from the referenced schema
before normalizeSchema runs. The existing normalizeReferenceSchema logic
then rewrites the property as allOf+default, making the default visible
as schema.getDefault() to all generators.

Change 2 (opt-in): new normalizer rule RESOLVE_SCHEMA_DEFAULTS=<strategy>
where <strategy> is LAST_WINS | NEAREST_WINS | ROOT_WINS | STRICT. When
enabled, ModelUtils.resolveDefault() is called after normalization for
every component schema and every property schema with no direct default.
The resolved value (if any) is written back via schema.setDefault().

Change 3: document both mechanisms in docs/default-resolution-strategy.md
under a new section 'How defaults are surfaced at codegen time'.

These changes cover ALL generators - the normalizer modifies the spec
model before any generator's toDefaultValue(Schema) runs, including the
~60 generators that override that method and do not call super.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… mutation

- Remove Change 1 normalizer block that copied defaults onto \ properties.
  It caused normalizeReferenceSchema to wrap as allOf, breaking typed generators
  like C# whose toDefaultValue cannot handle untyped allOf schemas.

- Add deep \ traversal loop in DefaultCodegen.fromProperty before calling
  toDefaultValue. ModelUtils.getReferencedSchema is single-level, so the loop
  iterates until stable, handling chains of arbitrary depth
  (A -> \ -> \ -> {type: string, default: x}).

- Remove now-redundant getReferencedSchema lines from AbstractJavaCodegen and
  AbstractKotlinCodegen toDefaultValue overrides; fromProperty already resolves
  the \ before calling toDefaultValue.

- Guard RESOLVE_SCHEMA_DEFAULTS rule in normalizeProperties to skip pure \
  schemas, preventing invalid OAS 3.0 \-with-sibling-default constructs.

- Update OpenAPINormalizerTest: testRefDefaultPropagation -> testRefDefaultNotPropagatedByNormalizer.
- Add two DefaultCodegenTest cases: single and chained \ default propagation.
- Update docs/default-resolution-strategy.md to reflect codegen-layer approach.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DefaultCodegen.fromProperty now always resolves \ chains before calling
toDefaultValue, so generators receive a concrete typed schema. Generators
that need type-name-based initialization (not OAS default) must use the
two-arg toDefaultValue(CodegenProperty, Schema) override and cp.isModel.

- CppTinyClientCodegen: add two-arg override returning cp.dataType + '()'
  for model types (C++ default construction). Remove dead \ branch from
  the single-arg method.

- CppTizenClientCodegen: same, returning 'new ' + cp.dataType + '()' for
  heap-allocated default construction.

- AbstractPythonCodegen: remove the \-detection block at the top of
  toDefaultValue(Schema p) -- it is now dead code since fromProperty
  pre-resolves \ chains before calling toDefaultValue.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The block was mistakenly removed as 'dead code'. While DefaultCodegen.fromProperty
pre-resolves \ chains before calling toDefaultValue, the single-arg
toDefaultValue(Schema) is also called directly from fromOperation (line 4666:
op.defaultResponse = toDefaultValue(responseSchema)) without any \
pre-resolution. If responseSchema is a \, the block is necessary for Python
to follow the ref and obtain the correct default value.

Restore the block with an explanatory comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ModelUtils.getReferencedSchema returns the input schema unchanged when a
ref cannot be resolved, making 'referenced == pForDefault' the natural
loop terminator. The extra prevSchema sentinel guarded against circular
\ which are invalid OpenAPI and do not need application-level
protection. Remove it for clarity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ion after ref pre-resolution

fromProperty now pre-resolves \ chains before calling toDefaultValue, so model
property schemas arrive fully resolved (no \). The old \ detection branch in
toDefaultValue(Schema) no longer fires, leaving defaultValue as null for model types.

Fix: in processModelVariable, when a property isModel and defaultValue is null, set
defaultValue = datatypeWithEnum + '()' so the C++ value-type member appears in the
constructor initializer list (e.g. 'category(Category())').

Also remove the now-dead \ branch from toDefaultValue(Schema) that was returning
std::make_shared<TypeName>() — processModelVariable was already converting it to
value-type initialization, but the source of truth is now the two-step pattern:
toDefaultValue returns null for unresolvable types, processModelVariable fills in
the C++ default construction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Picazsoo Picazsoo marked this pull request as ready for review May 19, 2026 12:12
@Picazsoo Picazsoo marked this pull request as draft May 19, 2026 12:13
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

3 issues found across 141 files

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="samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/EnumTest.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/EnumTest.cs:43">
P2: Constructor XML docs claim enum defaults are applied, but the constructor passes through `Option<T> = default` without applying them, creating a doc/behavior mismatch.</violation>
</file>

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

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java:348">
P1: RESOLVE_SCHEMA_DEFAULTS rule can be enabled with a null strategy when the user provides an invalid value (e.g. `true`), leading to a NullPointerException at runtime in ModelUtils.resolveDefault</violation>

<violation number="2" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java:670">
P1: Component schemas which are pure $ref may get a sibling `default` written, creating an invalid OAS 3.0 Reference Object. The `normalizeComponentsSchemas()` method resolves and writes defaults back onto normalized component schemas without checking whether the schema is a pure `$ref`, unlike the property-level normalization which correctly skips pure `$ref` schemas to avoid invalid OAS 3.0 Reference Objects.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic


// if RESOLVE_SCHEMA_DEFAULTS is enabled, compute and write back the effective
// default for allOf-composed component schemas that have no direct default.
if (getRule(RESOLVE_SCHEMA_DEFAULTS) && normalizedSchema.getDefault() == 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: Component schemas which are pure $ref may get a sibling default written, creating an invalid OAS 3.0 Reference Object. The normalizeComponentsSchemas() method resolves and writes defaults back onto normalized component schemas without checking whether the schema is a pure $ref, unlike the property-level normalization which correctly skips pure $ref schemas to avoid invalid OAS 3.0 Reference Objects.

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/OpenAPINormalizer.java, line 670:

<comment>Component schemas which are pure $ref may get a sibling `default` written, creating an invalid OAS 3.0 Reference Object. The `normalizeComponentsSchemas()` method resolves and writes defaults back onto normalized component schemas without checking whether the schema is a pure `$ref`, unlike the property-level normalization which correctly skips pure `$ref` schemas to avoid invalid OAS 3.0 Reference Objects.</comment>

<file context>
@@ -643,7 +662,17 @@ protected void normalizeComponentsSchemas() {
+
+                // if RESOLVE_SCHEMA_DEFAULTS is enabled, compute and write back the effective
+                // default for allOf-composed component schemas that have no direct default.
+                if (getRule(RESOLVE_SCHEMA_DEFAULTS) && normalizedSchema.getDefault() == null) {
+                    Object resolved = ModelUtils.resolveDefault(openAPI, normalizedSchema, resolveSchemaDefaultsStrategy);
+                    if (resolved != null) {
</file context>

resolveSchemaDefaultsStrategy = DefaultResolutionStrategy.valueOf(resolveSchemaDefaultsValue.trim().toUpperCase(java.util.Locale.ROOT));
rules.put(RESOLVE_SCHEMA_DEFAULTS, true);
} catch (IllegalArgumentException e) {
LOGGER.error("RESOLVE_SCHEMA_DEFAULTS rule must be one of LAST_WINS, NEAREST_WINS, ROOT_WINS, STRICT. Got: {}", resolveSchemaDefaultsValue);
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: RESOLVE_SCHEMA_DEFAULTS rule can be enabled with a null strategy when the user provides an invalid value (e.g. true), leading to a NullPointerException at runtime in ModelUtils.resolveDefault

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/OpenAPINormalizer.java, line 348:

<comment>RESOLVE_SCHEMA_DEFAULTS rule can be enabled with a null strategy when the user provides an invalid value (e.g. `true`), leading to a NullPointerException at runtime in ModelUtils.resolveDefault</comment>

<file context>
@@ -329,6 +338,16 @@ public void processRules(Map<String, String> inputRules) {
+                resolveSchemaDefaultsStrategy = DefaultResolutionStrategy.valueOf(resolveSchemaDefaultsValue.trim().toUpperCase(java.util.Locale.ROOT));
+                rules.put(RESOLVE_SCHEMA_DEFAULTS, true);
+            } catch (IllegalArgumentException e) {
+                LOGGER.error("RESOLVE_SCHEMA_DEFAULTS rule must be one of LAST_WINS, NEAREST_WINS, ROOT_WINS, STRICT. Got: {}", resolveSchemaDefaultsValue);
+            }
+        }
</file context>
Suggested change
LOGGER.error("RESOLVE_SCHEMA_DEFAULTS rule must be one of LAST_WINS, NEAREST_WINS, ROOT_WINS, STRICT. Got: {}", resolveSchemaDefaultsValue);
LOGGER.error("RESOLVE_SCHEMA_DEFAULTS rule must be one of LAST_WINS, NEAREST_WINS, ROOT_WINS, STRICT. Got: {}", resolveSchemaDefaultsValue);
rules.put(RESOLVE_SCHEMA_DEFAULTS, false);

/// <param name="enumString">enumString</param>
/// <param name="outerEnum">outerEnum</param>
/// <param name="outerEnumDefaultValue">outerEnumDefaultValue</param>
/// <param name="outerEnumDefaultValue">outerEnumDefaultValue (default to OuterEnumDefaultValue.Placed)</param>
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.

P2: Constructor XML docs claim enum defaults are applied, but the constructor passes through Option<T> = default without applying them, creating a doc/behavior mismatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/EnumTest.cs, line 43:

<comment>Constructor XML docs claim enum defaults are applied, but the constructor passes through `Option<T> = default` without applying them, creating a doc/behavior mismatch.</comment>

<file context>
@@ -40,9 +40,9 @@ public partial class EnumTest : IValidatableObject
         /// <param name="enumString">enumString</param>
         /// <param name="outerEnum">outerEnum</param>
-        /// <param name="outerEnumDefaultValue">outerEnumDefaultValue</param>
+        /// <param name="outerEnumDefaultValue">outerEnumDefaultValue (default to OuterEnumDefaultValue.Placed)</param>
         /// <param name="outerEnumInteger">outerEnumInteger</param>
-        /// <param name="outerEnumIntegerDefaultValue">outerEnumIntegerDefaultValue</param>
</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