fix: keep allOf-inherited "any type" maps from degrading to free-form object - #24547
fix: keep allOf-inherited "any type" maps from degrading to free-form object#24547donald wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
Good catch — fixed in 7416148. You are right on both points. The parser does reuse a single I keyed the set on the clone rather than dropping it: each clone node is distinct, so every occurrence now gets fixed, and a cheap guard remains. Added Full test suite and all 787 sample configs re-run — still no diff. |
… object
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 changed from "any" to "free-form object", so the same
property produced two different data types in the same run:
Base.style: { [key: string]: any; } // declared here
Child.style: { [key: string]: object; } // inherited via allOf
The allOf composition branch in DefaultCodegen.fromModel calls
mergeProperties(), which deep-copies every inherited property schema via
ModelUtils.cloneSchema(). cloneSchema delegates to AnnotationsUtils.clone,
a JSON round-trip, and swagger-core's deserializer materializes a
sub-schema that has no type as an ObjectSchema (type: object):
Schema inner = new Schema().description("can be any type");
Schema outer = new ObjectSchema().additionalProperties(inner);
// before: class = Schema, type = null, isFreeFormObject = false
// after: class = ObjectSchema, type = object, isFreeFormObject = true
getPrimitiveType() then matches the isFreeFormObject branch and returns
"object", never reaching the isAnyType branch that returns "AnyType".
cloneSchema already guards a custom top-level type across the round-trip
but does not cover sub-schemas. This extends the same idea: after cloning,
walk the schema tree and clear the type wherever the original did not
declare one (additionalProperties, items, properties, allOf/anyOf/oneOf,
not). Schemas that genuinely declare `type: object` are untouched.
Tests: three ModelUtilsTest cases (typeless additionalProperties, nested
typeless schemas, and a declared `type: object` that must stay an object)
plus an end-to-end TypeScriptFetchClientCodegenTest that asserts inherited
properties keep the same types as the ones declared on the parent.
Regenerating all 787 sample configs produces no diff.
Addresses review feedback on the visited set in restoreTypelessSubSchemas. The parser reuses a single Schema instance across several places in a spec (the InlineModelResolver tests document this for external types), while the JSON round-trip inside AnnotationsUtils.clone gives each of those places its own clone object. Keying the visited set on the original meant only the first occurrence got fixed up; every later one silently kept type: object, so the fix did not hold for the second location. Keying on the clone fixes every occurrence. The set is not needed for cycle protection either -- the clone is produced by JSON (de)serialization, so the original graph cannot contain cycles in the first place -- but keying on the clone keeps a cheap guard without the false skips. New test testCloneKeepsEveryOccurrenceOfAReusedTypelessSchemaTypeless reproduces the reported scenario; on the previous version it fails with "second kept type: object expected [null] but found [object]". Regenerating all 787 sample configs still produces no diff.
7416148 to
f6e331e
Compare
Fixes #24546
What
A property whose
additionalPropertiesschema declares notypeis an "any type" map. When such aproperty is inherited through
allOf, the generated value type silently changed from "any" to"free-form object" — so the same property produced two different data types in the same run:
Why
The
allOfcomposition branch inDefaultCodegen.fromModelcallsmergeProperties(), whichdeep-copies every inherited property schema via
ModelUtils.cloneSchema().cloneSchemadelegates toAnnotationsUtils.clone— a JSON round-trip — and swagger-core's deserializer materializes a sub-schemathat has no
typeas anObjectSchema(type: object):DefaultCodegen.getPrimitiveType()then matches theModelUtils.isFreeFormObject(...)branch and returns"object", never reaching theModelUtils.isAnyType(...)branch that returns"AnyType"(mapped toanyby the TypeScript generators).
This is generator-agnostic —
typescript-fetchis just where it is most visible.How
cloneSchema()already guards a custom top-level type across the round-trip, but does not coversub-schemas. This extends the same idea: after cloning, walk the schema tree and clear the type wherever
the original did not declare one (
additionalProperties,items,properties,allOf/anyOf/oneOf,not). An identity-based visited set guards against cycles.Schemas that genuinely declare
type: objectare untouched — there is an explicit test for that so the fixcannot over-reach.
Testing
ModelUtilsTest— 3 new cases: typelessadditionalProperties, nested typeless schemas(
properties/items), and a declaredtype: objectthat must stay an object.TypeScriptFetchClientCodegenTest.testAllOfInheritedFreeFormMapStaysAny— end-to-end, asserts theinherited properties on
Childcarry exactly the same types as the ones declared onBase.Verified that this test fails without the fix.
./bin/generate-samples.sh ./bin/configs/*.yaml) and./bin/utils/export_docs_generators.sh— no diff. The fix is inert on the entire existing samplecorpus; only the buggy case changes.
PR checklist
(Both scripts were run; neither produced any change, so there is nothing to commit beyond the fix and its tests.)
masterThe change is in the language-agnostic core (
ModelUtils), so it is not owned by one language committee.Pinging the TypeScript technical committee since that is where the symptom shows up:
@TiFu @taxpon @sebastianhaas @kenisteward @Vrolijkx @macjohnny @topce @akehir @petejohansonxo @amakhrov @davidgamero @mkusaka @joscha
CC also @wing328 as this touches shared codegen behaviour.
Summary by cubic
Fixes a bug where “any type” maps inherited via allOf were generated as free-form objects, causing parent and child models to disagree. Inherited properties now keep the correct
anyvalue type across generators (notablytypescript-fetch).ModelUtils.cloneSchema, after cloning, walk sub-schemas and cleartypewherever the original had none (additionalProperties,items,properties,allOf/anyOf/oneOf,not) with a visited set keyed on the clone to handle reused schemas; genuinetype: objectstays intact.ModelUtilsTestcases (typelessadditionalProperties, nested typeless schemas, and reused typeless schema occurrences) and an end-to-endtypescript-fetchtest; regenerating samples produced no diffs.Written for commit f6e331e. Summary will update on new commits.