Once upon a time, you have a Rails server. This server does all kinds of wonderful things, surely. One of those things is it takes json, parses it and stores it somewhere.

Here's my question to you: how do you parse the JSON and where? Do you do it in the controller? Do you do it like this?

class SomethingJsonController < ApplicationController
  def create
    Model.create(strong_params)
  end

  def update
    Model.update_attributes(strong_params)
  end

  private

  def strong_params
    params.require(:blah).permit(:blah, :blah, :blah)
  end
end

Sure. You can. But what happens when the JSON you get is all weird and nested? Even worse, what if it isn't a 1:1 mapping of your model? You end up with some intesnely unpleasant controller code. Let's look at an example to show you what I mean.

An example

OK. We'll say that you have an endpoint that talks to a service/device and takes a JSON blob that looks like this:

{
  "restaurant_id" : 13,
        "user_id" : 6,
      "dish_name" : "risotto con funghi",
    "description" : "repulsive beyond belief",
        "ratings" : {
                        "taste" : "terrible",
                        "color" : "horrendous",
                      "texture" : "vile",
                        "smell" : "delightful, somehow"
                    }
}

But your model doesn't directly map to that. In fact, it's flat and boring, like

# DishReview model

t.belongs_to  :restaurant
t.belongs_to  :user
t.string      :name # field name different from API (dish_name)
t.string      :description
t.string      :taste
t.string      :color
t.string      :texture
t.string      :smell

The problem

What many people would do (assuming you can't change the incoming JSON) is try to parse and modify the params in the controller, which ends up looking roughly like this:


class DishReviewController < BaseController

  def create
    review_params = get_review_params(params)
    @review = DishReview.new(review_params)
    if @review.save
      # return review
    else
      # return sad errors splody
    end
  end

  # rest of RUD

  protected

  def permitted_params
   [
      :restaurant_id,
      :user_id
      :dish_name,
      :description,
      :taste,
      :color,
      :texture,
      :smell
    ]
  end

  def get_review_params(params)
    review_params = params.require(:review)

    review_params[:name] ||= review_params.delete(:dish_name)

    ratings = review_params.delete(:ratings)
    if (ratings.present?)
      ratings.each{|rating, value| review_params[rating] = value if valid_rating?(rating) }
    end

    review_params.permit(permitted_params)
  end

  def valid_rating?(rating)
    [ "taste", "color", "texture", "smell" ].include? rating
  end
end

Man, that sure is a lot of non-controller code inside that controller. 30 lines, in fact. And that's if you have the same params coming in for all the actions. What if your update and create take different params? Then it'll get all nasty and you'll start shoving code into concerns; it'll be hard to read, hard to follow, maintain and refactor.

Our solution

But enough of this Negative Nancy talk. I have options! Well, just one, really. It's a gem, we call "Deserializer"!

Here at Avvo, my team are working on cross-server communication and building out new APIs as we scale our product. We serialize data using ActiveModelSerializer and it felt very frustrating having to parse the generated JSON by hand on the receiving side. So to ease the pain of perpetually having to mangle hashes in the controller, I wrote this gem.

So what does this "deserializer" of yours do, exaclty?

The Deserializer acts as the opposite of AMS. AMS takes an object, and converts it into JSON. The deserializer takes in params (incoming JSON), and converts them into model consumable data. It does not create an object out of those params. Really, it's a glorified hash mangler.

Great. Whatever. How does that help me?

Using the example above, let's look at what our code will look like with a deserializer

controller

class DishReviewsController < YourApiController::Base
  def create
    review_params = DishReviewDeserailzer.from_params(params)
    DishReview.create( review_params )
  end

  # RUD
end

"Wow!", you say, "That's so tidy and neat!". You are correct. And the deserializers aren't too bad either. Let's have a look

deserializers

# DishReviewDeserializer

module MyApi
  module V1
    class DishReviewDeserializer < Deserializer::Base
      attributes  :restaurant_id,
                  :user_id,
                  :description

      attribute   :name, key: :dish_name

      has_one :ratings, :deserializer => RatingsDeserializer

      def ratings
        object
      end

    end
  end
end

# RatingsDeserializer:

module MyApi
  module V1
    class RatingsDeserializer < Deserializer::Base

      attributes  :taste,
                  :color,
                  :texture,
                  :smell
    end
  end
end

"Hot dog!", you exclaim, "Those look just like my serializers on the other side! It's as if the interface is written to match that of AMS!" They sure do. And it sure is.

Now, not only are your concerns separated, but you can reason about what your code does and understand what data is coming in by just looking at the deserializers.

As a nice bonus, since the deserializer ignores undefined keys, you no longer have to strong param anything - but you still can if you want (because it's just a hash mangler). There's even a function to help you, MyDeserializer.permitted_params will give you the list of paramaters that the deserializer expects to get.

For more detailed info, feel free to RTM and contribute.