Bug Report Checklist
Description
In the python generator, an explicit per-schema additionalProperties: false in the spec is silently ignored when the global option disallowAdditionalPropertiesIfNotPresent=false is set. The unknown-field rejection loop in the generated model's from_dict is keyed solely off the global flag, never off the per-schema spec value.
The documented semantics of the flag say the opposite should happen:
If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications.
Per OAS / JSON Schema semantics, the flag should only fill in behavior when the spec is silent about additionalProperties. An explicit additionalProperties: false should always produce strict behavior regardless of the flag. As it stands, users who set disallowAdditionalPropertiesIfNotPresent=false (to get spec-compliant defaults) lose all strictness, including strictness the spec explicitly asks for.
Verified matrix (openapi-generator-cli v7.23.0, python generator, usePydanticV2=true):
spec declares additionalProperties: false on the schema |
disallowAdditionalPropertiesIfNotPresent |
rejection loop in generated from_dict |
| yes (OAS 3.1.0) |
false |
absent (bug) |
| yes (OAS 3.0.3) |
false |
absent (bug — not 3.1-specific) |
| yes (OAS 3.1.0) |
true |
present |
| no (OAS 3.1.0) |
true |
present (loop tracks only the flag) |
In the failing rows there is no other strictness marker in the generated model either — no pydantic extra="forbid", nothing. The explicit additionalProperties: false is entirely lost.
openapi-generator version
7.23.0 (docker image openapitools/openapi-generator-cli:v7.23.0). Not a regression as far as I can tell — the template gate on master is unchanged, so the issue still exists on latest master:
https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/python/model_generic.mustache#L495-L503
{{#disallowAdditionalPropertiesIfNotPresent}}
{{^isAdditionalPropertiesTrue}}
# raise errors for additional fields in the input
for _key in obj.keys():
if _key not in cls.__properties:
raise ValueError("Error due to additional fields (not defined in {{classname}}) in the input: " + _key)
{{/isAdditionalPropertiesTrue}}
{{/disallowAdditionalPropertiesIfNotPresent}}
The outer section is the global CLI option, so the loop can never be emitted for an individual schema when the option is false, no matter what the schema says.
OpenAPI declaration file content or url
{
"openapi": "3.1.0",
"info": {"title": "Repro", "version": "1.0.0"},
"paths": {
"/things": {
"post": {
"operationId": "createThing",
"requestBody": {
"required": true,
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Payload"}}}
},
"responses": {"204": {"description": "ok"}}
}
}
},
"components": {
"schemas": {
"Payload": {
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": {
"name": {"type": "string"},
"count": {"type": "integer"}
}
}
}
}
}
(Same result with "openapi": "3.0.3".)
Generation Details
docker run --rm -v $PWD:/work openapitools/openapi-generator-cli:v7.23.0 generate \
-g python -i /work/spec.json -o /work/out \
--additional-properties packageName=repro,usePydanticV2=true,disallowAdditionalPropertiesIfNotPresent=false,hideGenerationTimestamp=true
Steps to reproduce
- Save the spec above as
spec.json.
- Run the command above.
- Inspect
out/repro/models/payload.py, from_dict.
Actual output (disallowAdditionalPropertiesIfNotPresent=false, spec explicitly forbids additional properties):
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of Payload from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"name": obj.get("name"),
"count": obj.get("count")
})
return _obj
Payload.from_dict({"name": "x", "unexpected": 1}) succeeds and drops the unknown key, even though the spec declares additionalProperties: false.
Expected output (what the same schema produces when the flag is true):
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of Payload from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
# raise errors for additional fields in the input
for _key in obj.keys():
if _key not in cls.__properties:
raise ValueError("Error due to additional fields (not defined in Payload) in the input: " + _key)
_obj = cls.model_validate({
"name": obj.get("name"),
"count": obj.get("count")
})
return _obj
Expected behavior: the rejection loop (or equivalent, e.g. pydantic extra="forbid") should be emitted whenever the schema explicitly declares additionalProperties: false, regardless of disallowAdditionalPropertiesIfNotPresent. The flag should only decide the behavior for schemas that don't mention additionalProperties at all.
Related issues/PRs
Suggest a fix
In model_generic.mustache, the rejection loop is gated on the global {{#disallowAdditionalPropertiesIfNotPresent}} option. It should additionally be emitted when the model's schema has an explicit additionalProperties: false (e.g. gate on a per-model property such as isAdditionalPropertiesTrue being false because the spec said so, which likely requires the codegen to distinguish "explicit false" from "absent" — CodegenModel/DefaultCodegen already track isAdditionalPropertiesTrue, but there is currently no per-model "explicitly false" signal exposed to the template).
Bug Report Checklist
Description
In the
pythongenerator, an explicit per-schemaadditionalProperties: falsein the spec is silently ignored when the global optiondisallowAdditionalPropertiesIfNotPresent=falseis set. The unknown-field rejection loop in the generated model'sfrom_dictis keyed solely off the global flag, never off the per-schema spec value.The documented semantics of the flag say the opposite should happen:
Per OAS / JSON Schema semantics, the flag should only fill in behavior when the spec is silent about
additionalProperties. An explicitadditionalProperties: falseshould always produce strict behavior regardless of the flag. As it stands, users who setdisallowAdditionalPropertiesIfNotPresent=false(to get spec-compliant defaults) lose all strictness, including strictness the spec explicitly asks for.Verified matrix (openapi-generator-cli v7.23.0,
pythongenerator,usePydanticV2=true):additionalProperties: falseon the schemadisallowAdditionalPropertiesIfNotPresentfrom_dictfalsefalsetruetrueIn the failing rows there is no other strictness marker in the generated model either — no pydantic
extra="forbid", nothing. The explicitadditionalProperties: falseis entirely lost.openapi-generator version
7.23.0 (docker image
openapitools/openapi-generator-cli:v7.23.0). Not a regression as far as I can tell — the template gate on master is unchanged, so the issue still exists on latest master:https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/python/model_generic.mustache#L495-L503
The outer section is the global CLI option, so the loop can never be emitted for an individual schema when the option is
false, no matter what the schema says.OpenAPI declaration file content or url
{ "openapi": "3.1.0", "info": {"title": "Repro", "version": "1.0.0"}, "paths": { "/things": { "post": { "operationId": "createThing", "requestBody": { "required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Payload"}}} }, "responses": {"204": {"description": "ok"}} } } }, "components": { "schemas": { "Payload": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": {"type": "string"}, "count": {"type": "integer"} } } } } }(Same result with
"openapi": "3.0.3".)Generation Details
docker run --rm -v $PWD:/work openapitools/openapi-generator-cli:v7.23.0 generate \ -g python -i /work/spec.json -o /work/out \ --additional-properties packageName=repro,usePydanticV2=true,disallowAdditionalPropertiesIfNotPresent=false,hideGenerationTimestamp=trueSteps to reproduce
spec.json.out/repro/models/payload.py,from_dict.Actual output (
disallowAdditionalPropertiesIfNotPresent=false, spec explicitly forbids additional properties):Payload.from_dict({"name": "x", "unexpected": 1})succeeds and drops the unknown key, even though the spec declaresadditionalProperties: false.Expected output (what the same schema produces when the flag is
true):Expected behavior: the rejection loop (or equivalent, e.g. pydantic
extra="forbid") should be emitted whenever the schema explicitly declaresadditionalProperties: false, regardless ofdisallowAdditionalPropertiesIfNotPresent. The flag should only decide the behavior for schemas that don't mentionadditionalPropertiesat all.Related issues/PRs
disallowAdditionalPropertiesIfNotPresent=falsevs explicit spec values).disallowAdditionalPropertiesIfNotPresentdefault tofalse#21169 requests changing the flag's default tofalse— which would make this bug the default behavior for every explicitadditionalProperties: falseschema.Suggest a fix
In
model_generic.mustache, the rejection loop is gated on the global{{#disallowAdditionalPropertiesIfNotPresent}}option. It should additionally be emitted when the model's schema has an explicitadditionalProperties: false(e.g. gate on a per-model property such asisAdditionalPropertiesTruebeing false because the spec said so, which likely requires the codegen to distinguish "explicit false" from "absent" —CodegenModel/DefaultCodegenalready trackisAdditionalPropertiesTrue, but there is currently no per-model "explicitly false" signal exposed to the template).