Skip to content

[BUG] allOf-inherited "any type" additionalProperties map degrades to free-form object #24546

Description

@donald

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

A property whose additionalProperties schema declares no type is an "any type" map. When such a property is
inherited through allOf, the generated value type silently changes from "any" to "free-form object".

The same property, declared once and inherited once, produces two different data types in the same run.

openapi-generator version

7.25.0-SNAPSHOT, built from master at commit 4aed9c1c635.

Not a recent regression: the responsible code path (mergePropertiescloneSchema) has been in place
for several releases.

OpenAPI declaration file content or url
openapi: 3.0.3
info:
  title: allOf inherited free-form map
  version: 1.0.0
paths:
  /child:
    get:
      operationId: getChild
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Child"
components:
  schemas:
    Base:
      required:
        - requiredLoose
      properties:
        requiredLoose:
          type: object
          additionalProperties:
            description: can be any type
        optionalLoose:
          type: object
          additionalProperties:
            description: can be any type
        realObject:
          type: object
          additionalProperties:
            type: object
    Child:
      allOf:
        - $ref: "#/components/schemas/Base"
      properties:
        extra:
          type: string
Generation Details
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
  -i spec.yaml -g typescript-fetch -o out

typescript-fetch is used here because the difference is most visible (any vs object),
but the root cause is in DefaultCodegen/ModelUtils and is therefore generator-agnostic.

Steps to reproduce
  1. Save the spec above as spec.yaml
  2. Run the command above
  3. Compare out/models/Base.ts with out/models/Child.ts

Actual output

// models/Base.ts
requiredLoose: { [key: string]: any; };
optionalLoose?: { [key: string]: any; };
realObject?: { [key: string]: object; };

// models/Child.ts  <-- inherited from Base via allOf
requiredLoose: { [key: string]: object; };   // changed
optionalLoose?: { [key: string]: object; };  // changed
realObject?: { [key: string]: object; };

Expected output

Child should carry the exact same types as Base for the inherited properties:

requiredLoose: { [key: string]: any; };
optionalLoose?: { [key: string]: any; };
realObject?: { [key: string]: object; };

Note realObject (which genuinely declares type: object) is correct in both — only the
typeless / "any type" schemas are affected.

Suggest a fix

The allOf composition branch in DefaultCodegen.fromModel calls mergeProperties():

https://github.com/OpenAPITools/openapi-generator/blob/4aed9c1c635/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java#L2854

mergeProperties() deep-copies every inherited property schema via ModelUtils.cloneSchema(), which
delegates to AnnotationsUtils.clone() — a JSON round-trip. swagger-core's deserializer materializes a
sub-schema that has no type as an ObjectSchema (i.e. type: object).

This can be observed directly:

Schema inner = new Schema().description("can be any type");
Schema outer = new ObjectSchema().additionalProperties(inner);

// before: class = Schema,       type = null,   isFreeFormObject = false
Schema cloned = ModelUtils.cloneSchema(outer, false);
// after:  class = ObjectSchema, type = object, isFreeFormObject = true

Consequently, in DefaultCodegen.getPrimitiveType() the ModelUtils.isFreeFormObject(...) branch now
matches and returns "object", so execution never reaches the ModelUtils.isAnyType(...) branch that
would return "AnyType" (mapped to any by the TypeScript generators, Object/interface{}/... elsewhere).

cloneSchema() already has a guard that preserves a custom top-level type across the round-trip, but it
does not cover sub-schemas. Extending the same idea — after cloning, walk the schema tree and clear the type
wherever the original did not declare one — fixes it.

I have a patch with tests ready and will open a PR shortly.

Related issues/PRs

Searched open and closed issues/PRs for cloneSchema, mergeProperties, and allOf + free-form/additionalProperties
combinations; I could not find an existing report. #22150 is also about allOf-inherited properties but concerns
Spring bean-validation annotations, not the resolved data type.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions