Repository navigation
Conversation
With update_only, `*_attributes=` updates the existing nested model with a strict `assign_attributes`, so one unknown key in the nested payload made the whole cast raise. The parent now passes its leniency on to the model it updates in place; direct assignment and ActiveRecord parents stay strict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DmitryTsepelev
left a comment
There was a problem hiding this comment.
Thanks for the quick follow-up. The cast case from #240 works now, and CI is green.
I'd like one change in behaviour before merging, explained inline. Right now the same *_attributes= call keeps unknown keys in one situation and raises in another. The other inline comments are small.
Please also add a line for this PR under ## master in CHANGELOG.md. Your author link is already at the bottom of the file from #240.
| # A parent collecting unknown keys during a cast (see UnknownAttributeAssignment) | ||
| # lets the nested model it updates in place collect them too. | ||
| def update_nested_model(model, *args, **kwargs) | ||
| return model.assign_attributes(*args, **kwargs) unless @collecting_unknown_attributes |
There was a problem hiding this comment.
With update_only, the same assignment now behaves differently depending on whether the nested model already exists.
When there is no supplier yet, *_attributes= goes through send("supplier=", ...), which casts through the type. The type keeps unknown keys. When a supplier exists, this method only keeps them if the parent is being built by a type cast at that moment. In every other case the same call raises:
class Settings
include StoreModel::Model
attribute :supplier, Supplier.to_type
accepts_nested_attributes_for :supplier, update_only: true
end
Settings.new(supplier_attributes: { title: "a", extra: 1 })
# keeps "extra" in supplier.unknown_attributes
settings = Settings.new(supplier: { title: "a" })
settings.supplier_attributes = { extra: 1 }
# => ActiveModel::UnknownAttributeError: unknown attribute 'extra' for SupplierMaster behaves the same way here, so this isn't new. But this PR is the place to make the two paths agree.
I'd make the in-place update always keep unknown keys, the same way the replacement path does. That also means this method no longer needs to read the parent's @collecting_unknown_attributes. That variable belongs to UnknownAttributeAssignment, and NestedAttributes is also mixed into ActiveRecord models, which never set it. So for an ActiveRecord parent the existing-model case is always strict today, and this change would fix that too.
One thing to watch: an ActiveRecord parent can't call a protected method on the nested model, so the nested model will need a public entry point for this. Marking it :nodoc: would keep it out of the docs.
| expect(configuration.supplier.unknown_attributes).to eq("extra" => 1) | ||
| end | ||
|
|
||
| it "raises for unknown keys assigned directly to an existing model" do |
There was a problem hiding this comment.
This spec records the split described in the other comment as the intended behaviour. The same assign_attributes(supplier_attributes: { extra: 1 }) succeeds when supplier is nil, and this spec only covers the case where it raises. With the change I suggested, it would become "keeps unknown keys assigned to an existing model", and it would be worth adding an ActiveRecord-parent example next to it.
|
|
||
| # A parent collecting unknown keys during a cast (see UnknownAttributeAssignment) | ||
| # lets the nested model it updates in place collect them too. | ||
| def update_nested_model(model, *args, **kwargs) |
There was a problem hiding this comment.
This method ends up public, because it comes after the class-level private and there's no instance-level private above it. That adds update_nested_model to the public API of every StoreModel and every ActiveRecord model that includes NestedAttributes, and it could clash with a method of the same name there. Could you make it private?
|
|
||
| protected | ||
|
|
||
| def collecting_unknown_attributes |
There was a problem hiding this comment.
Small naming point. model.collecting_unknown_attributes { ... } reads like a getter, because it has the same name as the instance variable it changes. Something like collect_unknown_attributes { ... } would show it takes a block, and it would match UnknownAttributeAssignment.collect.
Follow-up to #240.
With
update_only: true,*_attributes=updates the existing nested model withassign_attributes, which is strict, so an unknown key in the nested payload made the whole cast raise:Now, when the parent is being built by a StoreModel type, the nested model it updates in place collects unknown keys the same way. Assigning to the nested model directly, or from an ActiveRecord parent, is still strict.
🤖 Generated with Claude Code