Rails nested attributes are not creating an object from JSON string inside a hidden form input
Problem
The error you are receiving AssociationTypeMismatch
is caused by putting origin:
and destination:
in your strong_params. Rails thinks you are trying to associate objects much like you would do @post.comment = @comment
.
Even with proper serialization & deserialization of your params this approach won't work. Rails sees what you are currently trying with strong_params as this:
# Not deserialized@package.origin = '{ \"address\":\"Kimmage, Dulbin, Ireland\", ... }'# Deserialized. However, this still won't work.@package.origin = { address: "Kimmage, Dublin, Ireland", ...}
Rails wants an object in both cases. You can test this by going into your console using the properly deserialized case:
$ rails cirb(main): p = Package.newirb(main): p.destination = { address: "Kimmage, Dublin, Ireland" } # => Throws ActiveRecord::AssociationTypeMismatch.
So, why isn't it working? Because instead of passing it an actual object, Rails interprets what you've passed as a string or a hash. In order to associate objects through strong_params, Rails looks for and uses the accepts_nested_attributes
method (which you've tried). However, this won't work out for you as explained below.
The problem here is the way you are trying to associate your data. Using accepts nested attributes is to associate and save child objects through a parent object. In your case you are trying to associate and save two parents objects (origin & destination) through a child object (package) using the accepts_nested_attributes_for method. Rails won't work this way.
First line from the docs (emphasis mine):
Nested attributes allow you to save attributes on associated records through the parent.
In your code, you're trying to associate and save/update it through the child.
Solutions
Solution 1
What you would need is the origin_id
and location_id
in your form, excluding accepts_nested_attributes
from your model since you won't need it, and then saving your package using the ID's:
params.require(:package).permit(:width, :length, :height, :whatever_else, :origin_id, :location_id)
Then, using AJAX requests before your form is submitted you insert the origin_id
and destination_id
of those two locations into hidden fields. You can use a find_or_create_by method to create those locations upon retrieval if they do not exist yet.
Solution 2
- Find or create your parent resources
@destination & @origin
in a before_action in your controller - Associate the
@origin
and@destination
to the@package
You will not need to accept_nested_attributes_for anything. You can save the package as you would normally (ensure to modify package_params).
class PackagesController < ApplicationController before_action :set_origin, only: [:create] before_action :set_destination, only: [:create] def create @package = current_user.packages.build(package_params) @package.destination = @destination @package.origin = @origin if @package.save # Do whatever you need else # Do whatever you need end endprivate # Create the package like you normally would def package_params params.require(:package).permit( :state, :delivery_date, :length, :height, :width, :weight) end def set_origin # You can use Location.create if you don't need to find a previously stored origin @origin = Location.find_or_create_by( address: params[:package][:origin][:address], lat: params[:package][:origin][:lat], lng: params[:package][:origin][:lng], ) end def set_destination # You can use Location.create if you don't need to find a previously stored destination @destination = Location.find_or_create_by( address: params[:package][:destination][:address], lat: params[:package][:destination][:lat], lng: params[:package][:destination][:lng], ) endend
To ensure you have a package with a valid origin and destination then validate that in your model:
class Package < ActiveRecord::Base validates :origin, presence: true validates :destination, presence: true validates_associated :origin, :destinationend