Rails nested attributes are not creating an object from JSON string inside a hidden form input Rails nested attributes are not creating an object from JSON string inside a hidden form input json json

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