[ruby] fix: build_from_hash drops every attribute inherited from an allOf parent - #24892
[ruby] fix: build_from_hash drops every attribute inherited from an allOf parent#24892wiebren wants to merge 5 commits into
Conversation
A discriminator allOf child generates as a real subclass, but build_from_hash iterated only openapi_types/attribute_map - class methods that dispatch on the child class in every frame of the ancestry, so a parent's attributes were never mapped from the wire hash: a required inherited attribute made deserialization raise ArgumentError, an optional one came back silently nil. The pre-existing super(attributes) call could not help: its result was discarded, and it iterated the child's own types anyway. Walk the ancestry merging each ancestor's openapi_types and attribute_map (the child wins on a name clash) and drop the discarded-result super call. RubyClientCodegenTest asserts the walk on the generated two-level Lizard < Reptile < Pet fixture (fails without the template change), and the three petstore custom base_object specs now deserialize a Cat that carries its parent's attributes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
There was a problem hiding this comment.
1 issue found across 220 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/ruby-faraday/lib/petstore/models/dog.rb">
<violation number="1" location="samples/client/petstore/ruby-faraday/lib/petstore/models/dog.rb:129">
P2: When a consumer subclasses a generated model but overrides its map and initializer, this ancestry walk passes parent fields that the subclass cannot accept. Restrict the walk to generated allOf inheritance or filter the transformed attributes through the receiver’s acceptable attribute map.</violation>
</file>
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Re-trigger cubic
| map = attribute_map | ||
| klass = superclass | ||
| while klass.respond_to?(:openapi_types) | ||
| types = klass.openapi_types.merge(types) |
There was a problem hiding this comment.
P2: When a consumer subclasses a generated model but overrides its map and initializer, this ancestry walk passes parent fields that the subclass cannot accept. Restrict the walk to generated allOf inheritance or filter the transformed attributes through the receiver’s acceptable attribute map.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/ruby-faraday/lib/petstore/models/dog.rb, line 129:
<comment>When a consumer subclasses a generated model but overrides its map and initializer, this ancestry walk passes parent fields that the subclass cannot accept. Restrict the walk to generated allOf inheritance or filter the transformed attributes through the receiver’s acceptable attribute map.</comment>
<file context>
@@ -118,20 +118,30 @@ def hash
+ map = attribute_map
+ klass = superclass
+ while klass.respond_to?(:openapi_types)
+ types = klass.openapi_types.merge(types)
+ map = klass.attribute_map.merge(map)
+ klass = klass.superclass
</file context>
The serialization direction has the identical defect: attribute_map and openapi_nullable dispatch on the child class in every ancestor frame, so the inherited super chain only repeated the child's own attributes and a round-tripped allOf child lost its parent's fields again on the way out. Walk the ancestry the same way build_from_hash now does; for a model without a generated parent the walk does not run and the output is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
|
cubic's first remark was a real catch, now fixed: On the other remark (a consumer subclass with its own |
There was a problem hiding this comment.
All reported issues were addressed across 219 files (changes from recent commits).
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Re-trigger cubic
…nd Set loads Two findings from review: the merged-nullable union let a parent's nullable flag survive a child redeclaring the attribute as non-nullable - an ancestor's entry now only applies to attributes no nearer class declares - and the walk evaluates openapi_nullable eagerly for every to_hash call, so the generated models require 'set' explicitly instead of leaning on the Set builtin autoload rubies before 3.2 do not have. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz
|
Both of cubic's round-2 findings were real and are fixed:
All ruby samples regenerated; RubyClientCodegenTest pins the corrected walk and the require. |
|
@wiebren thanks for all the PRs to improve this project. When you've time, can you please PM me via Slack for a quick discussion on these PRs? https://join.slack.com/t/openapi-generator/shared_invite/zt-36ucx4ybl-jYrN6euoYn6zxXNZdldoZA |
There was a problem hiding this comment.
1 issue found across 262 files (changes from recent commits).
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/echo_api/ruby-httpx/lib/openapi_client/models/string_enum_ref.rb">
<violation number="1" location="samples/client/echo_api/ruby-httpx/lib/openapi_client/models/string_enum_ref.rb:14">
P3: `require 'set'` is added unconditionally to model.mustache, so enum models like StringEnumRef also get it, but enums render through partial_model_enum_class.mustache and never use Set (only partial_model_generic's openapi_nullable does). The require is harmless but is dead code in every generated enum file; scope it to the non-enum branch (`{{^isEnum}}`) or move it into partial_model_generic.</violation>
</file>
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| =end | ||
|
|
||
| require 'date' | ||
| require 'set' |
There was a problem hiding this comment.
P3: require 'set' is added unconditionally to model.mustache, so enum models like StringEnumRef also get it, but enums render through partial_model_enum_class.mustache and never use Set (only partial_model_generic's openapi_nullable does). The require is harmless but is dead code in every generated enum file; scope it to the non-enum branch ({{^isEnum}}) or move it into partial_model_generic.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/ruby-httpx/lib/openapi_client/models/string_enum_ref.rb, line 14:
<comment>`require 'set'` is added unconditionally to model.mustache, so enum models like StringEnumRef also get it, but enums render through partial_model_enum_class.mustache and never use Set (only partial_model_generic's openapi_nullable does). The require is harmless but is dead code in every generated enum file; scope it to the non-enum branch (`{{^isEnum}}`) or move it into partial_model_generic.</comment>
<file context>
@@ -11,6 +11,7 @@
=end
require 'date'
+require 'set'
require 'time'
</file context>
A discriminator-based
allOfchild generates as a real subclass (class Cat < Animal), butits
build_from_hashmaps only the child's own attributes.openapi_typesandattribute_mapare class methods that dispatch on the child class in every frame of theancestry — including inside the parent's
build_from_hash— so the parent's attributes arenever transformed out of the wire hash:
nilExecuted against the generated petstore sample, current master vs this PR:
Found while comparing generators against a production registry API: discriminator-mapped
error models came back without their parent's
typeandmessage— both optional there, sothe loss was silent.
Why the existing
super(attributes)didn't save itThe child's
build_from_hashalready calledsuper(attributes)— but its return value wasdiscarded, and because
openapi_types/attribute_mapdispatch onself(the child) even inthe parent's frame, the call only built a second instance from the child's own attributes and
threw it away. It is removed rather than kept as dead code.
The fix
base_object.mustachewalks the ancestry, merging each ancestor'sopenapi_typesandattribute_map(the child's own declaration wins on a name clash):This mirrors the
acceptable_attribute_mapancestry merge the generatedinitializealreadyuses to accept those keys — the initializers were always willing to set inherited
attributes;
build_from_hashjust never handed them over.Tests
RubyClientCodegenTest#testBuildFromHashMapsInheritedAttributes— generates the two-levelLizard < Reptile < Petchain from the existing3_0/allOf_composition_discriminator.yamlfixture and asserts the walk is in place. Fails without the template change (verified by
stashing only the template).
spec/custom/base_object_spec.rbin the three petstore samples (typhoeus, faraday, httpx)gains a regression case deserializing a
Catthat carries its parent'sclassName/color;runs green locally.
PR checklist
./bin/generate-samples.sh ./bin/configs/ruby*.yaml).Every ruby-client model re-renders
build_from_hash, hence the broad but mechanicalsample diff.
ruby-nextgenis unaffected — its models do not usebuild_from_hash.Generated with Claude Code
Summary by cubic
Fixes the Ruby client so a discriminator-based
allOfchild keeps the attributes inherited from its parents in both deserialization and serialization. Previously a required inherited attribute raised on deserialize, an optional one came back silentlynil, and a round-tripped model lost its parent's fields again on the way out.build_from_hashandto_hashnow walk the ancestry, merging each ancestor'sopenapi_types,attribute_map, and nullability with the child's own declaration winning on a name clash.require 'set'explicitly, since the nullability walk evaluates eagerly and older rubies may not autoloadSet.super(attributes)call is removed.Lizard < Reptile < Petfixture and regression specs in the typhoeus, faraday, and httpx petstore samples.Written for commit 69d6e25. Summary will update on new commits.