OpenAPITools / OpenAPITools/openapi-generator
[BUG] allOf-inherited "any type" additionalProperties map degrades to free-form object
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
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 (mergeProperties → cloneSchema) 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
- Save the spec above as
spec.yaml - Run the command above
- Compare
out/models/Base.tswithout/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():
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with DefaultCodegen.fromModel and its mergeProperties call, then trace ModelUtils.cloneSchema and getPrimitiveType. Reproduce the issue with the supplied OpenAPI spec using the typescript-fetch generator, and verify that inherited typeless additionalProperties schemas retain the any mapping while explicitly typed object schemas remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, typescript
- Domain
- api, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100