[ruby] fix: build_from_hash drops every attribute inherited from an allOf parent

Merged
WI
2mo ago
+4699-1764

A discriminator-based allOf child generates as a subclass (class Cat < Animal), but its build_from_hash maps only the child's own attributes:

  • a required inherited attribute makes deserialization raise;
  • an optional inherited attribute silently comes back nil.
Petstore::Cat.build_from_hash({ 'className' => 'Cat', 'color' => 'black', 'declawed' => true })
# master:   ArgumentError: class_name cannot be nil
# this PR:  #<Petstore::Cat class_name="Cat", color="black", declawed=true>

to_hash has the mirror defect: a round-tripped child loses its parent's attributes on the way out. Reproduced on current master.

Why super(attributes) didn't help

openapi_types and attribute_map dispatch on the child class even in the parent's frame, so the super call only built a second instance from the child's own attributes and discarded it. It is removed.

Fix

partial_model_generic.mustache adds two class methods that use the same idiom as the existing acceptable_attribute_map (the nearest declaration wins):

def self.acceptable_openapi_types      # without a generated parent: openapi_types
  superclass.acceptable_openapi_types.merge(openapi_types)
end
def self.acceptable_openapi_nullable   # without a generated parent: openapi_nullable
  (superclass.acceptable_openapi_nullable - attribute_map.keys) | openapi_nullable
end

The delegation to superclass is gated on {{#parentModel}} (a generated parent), not {{#parent}}. This covers these helpers, acceptable_attribute_map and the initializer's super(attributes). An alias model such as ArrayAlias < Array has a parent but no generated parent, so every model gets a base case; array_alias.rb in the alias-as-model sample is the only file this gate changes.

In base_object.mustache, build_from_hash iterates acceptable_openapi_types and looks keys up in acceptable_attribute_map, the map the generated initializer validates against, so new only receives attributes it accepts. to_hash iterates acceptable_attribute_map and checks acceptable_openapi_nullable. A consumer subclass that narrows attribute_map behaves as on master.

Tests

  • RubyClientCodegenTest#testBuildFromHashMapsInheritedAttributes generates Lizard < Reptile < Pet from 3_0/allOf_composition_discriminator.yaml and asserts the merge methods, their use in build_from_hash/to_hash, and that the discarded super(attributes) is gone. Fails without the fix.
  • RubyClientCodegenTest#testAliasModelDoesNotDelegateToItsBuiltInSuperclass generates 3_0/features/generate-alias-as-model.yaml and asserts that ArrayAlias < Array has base-case helpers and no superclass/super(attributes) delegation.
  • spec/custom/base_object_spec.rb in the typhoeus, faraday and httpx petstore samples: a Cat round trip that keeps className/color, and a regression guard for a subclass that narrows attribute_map.

PR checklist

  • Read the contribution guidelines.
  • Built the project and updated samples (./bin/generate-samples.sh ./bin/configs/ruby*.yaml). Every ruby-client model changes, so the sample diff is broad but mechanical. ruby-nextgen is unaffected.
  • Technical committee: @cliffano @zlx @autopp

Summary by cubic

Fixes build_from_hash and to_hash in the Ruby client dropping attributes a model inherits from its allOf parents. A requirement bug where the attribute is:

  • A discriminator has its parent's attributes (fixed and a parent's approach)
  • child < parent raises build_class:
Petstore::Cat.build_from_hash({ 'className' => 'Cat', 'color' => 'black', 'declawed' => true })
# master:   ArgumentError: class_name cannot be nil                     # this PR:  #<Petstore::Cat class_name="Cat", color="black", declawed=true>

This approach adds two class methods with the same parent-gated idiom as the existing acceptable_attribute_map (stable attribute for classes whose superclass is parentModel); the children).---


Generated with Claude Code