-
You're not in the zone!
Working with the SQL 'time' type in Ruby on Rails.
-
GraphQL Schema Validation Talk - Boston ReactJS Meetup
-
The case against React snapshot testing... and what you can do instead!
-
Serialization in Rails
So the time has finally come. You knew it would happen some day, but you’d be prepared. Your app would be organized and fast and bulletproof, ready for anything! Then out of nowhere, right in the middle of all your best laid plans, the front-end team shows up on your lawn with a keg of High Life and some turntables and demands JAVASCRIPT NOW. You tell them ‘Be cool, young bloods. We can do JSON.’ And that’s how it begins.
Our front-end team at the officeLuckily, generating JSON in Rails is easy! All you need to do is add one line:
render json: my_object
That’s it. Now the Javascript folks can play around with their widgets, and you can go back to grumbling about ‘service objects’.
A few more weeks go by, and the mobile app team comes knocking at your door. You think to yourself “when did we get a mobile app team?”, but let them in anyway because they seem nice. They start talking about building an API, and you mumble something about how we’ve only just met and should take things slow. But man, they sure are convincing. You say ‘yes’. You have a couple beers with them and talk it over….. And you quickly realize that you’re gonna need a bigger boat. It’s time to start paying attention to how data is being serialized.
When you want more control over what your JSON looks like, you have a few different options in Rails land. You can use a templating system like RABL or jBuilder, create an object and manually transmogrify it using
to_json
, or useActiveModelSerializers
(to_json
, but fancy).Templating
Using a templating system has a very Rails-y MVC feel to it, so it may seem like the natural first choice for JSON-making. You create a ‘view’ file that is named after the corresponding controller action and plop it in a folder along with all the other view files for that controller. The JSON view has access to instance variables and helper methods, like any other Rails view. But unlike web pages that usually have unique markup, JSON responses are often built up from objects that get re-used in multiple places. Which means your nice name-spaced JSON view files often end up just rendering shared partials from some other folder. This creates a whole buncha extra files and folders, which gets annoying pretty quickly.
Hashing it out
Creating a Ruby object and converting it to JSON feels like a more, uh, literal approach. This is usually done by creating a Ruby hash and calling
.to_json
on it:> { name: "Fred", address: { city: "Boston", state: "MA" } }.to_json => "{\"name\":\"Fred\",\"address\":{\"city\":\"Boston\",\"state\":\"MA\"}}"
This is nice in a way, because you get a visual representation in Ruby of what the JSON object will look like. But after a while, you end up with a bunch of hash cow-pies everywhere that are a total pain to clean up. I’ve been down this road on projects before, and even if you’re super-duper organized, it just ain’t worth it. So Ruby hashes are out.
ActiveModelSerializers
A more civilized approach is to use ActiveModelSerializers. Right out of the box, you get a nice, easy to understand syntax for object associations, via
has_many
,has_one
, andbelongs_to
. Odds are that you’re already usingActiveRecord
to interact with your data layer, so this language should feel pretty natural. The conventions make sense, but can be easily customized. Adding custom key names, snake/camel casing things, and swapping out adapters is easy. Each serializer is a Ruby class, so all those methods that exist only to massage data for serialization finally have a place to live. A pretty great solution overall. And we haven’t even gotten to the kicker…The speed issue
Code that is organized and easy to understand is a great thing to have. But if that code is slow, no one will care about the amazing class architecture. So how do these different serialization options perform? Let’s take two identical soon-to-be-JSON objects, one using jBuilder, and one using ActiveModelSerializers:
# jBuilder - app/views/api/_address.json.jbuilder json.(address, :id, :name, :street, :city, :state, :zip, :short_address, :full_address, :search_address, :delivery_instructions, :on_site_contact, :latitude, :longitude, :phone, :residence) # AMS module Api class AddressSerializer < ActiveModel::Serializer attributes :id, :name, :street, :city, :state, :zip, :short_address, :full_address, :search_address, :delivery_instructions, :on_site_contact, :latitude, :longitude, :phone, :residence end end
And generate them 1000 times each:
view_context = ApplicationController.view_context_class.new( "#{Rails.root}/app/views" ) JbuilderTemplate.encode(view_context) do |json| 1000.times do |i| json.partial! "api/address", address: address end end 1000.times do |i| Api::AddressSerializer.new(address).to_json end
The results?
jBuilder: 4523.67 ms AMS: 178.94 ms
Whoa, not even close. Other folks have reported this as well. Plain ol’
to_json
is a bit faster, but the benefits of using ActiveModelSerializers more than make up for it. I think we have a winner!There are some great posts on how to get up and running with AMS, so I won’t go into detail here. Usage is pretty straight-forward overall, but there are still some pot holes that you may run over at some point. Here are a couple that I’ve come across.
How to Stay Single
Bad dating advice, but a great way to keep your code syntax clean! Picture a controller action that returns JSON for an
address
object, serialized using AMS.def show address = Address.find(params[:id]) render json: address, serializer: ::Api::AddressSerializer end
The syntax is nice and simple. But what if we need to provide multiple top-level JSON objects in the same response? Then things start to look not-so-nice.
def show address = Address.find(params[:id]) business = Business.find(params[:business_id]) user = find_user render json: { address: Api::AddressSerializer.new(address).as_json, business: Api::BusinessSerializer.new(business).as_json, user: Api::UserSerializer.new(user).as_json } end
Ugh, we’re back to using hash literals again. No bueno.
AMS likes single objects, so why not use one? Let’s treat the JSON response as a serializable object, and create a separate serializer and object wrapper to handle everything. We can even put them all in one file!
module Api class AddressResponseSerializer < ActiveModel::Serializer Inputs = Struct.new(:address, :business, :user) do alias :read_attribute_for_serialization :send end has_one :address, serializer: ::Api::AddressSerializer has_one :business, serializer: ::Api::BusinessSerializer has_one :user, serializer: ::Api::UserSerializer end end
The three objects we need to serialize get wrapped in a simple
Struct
, and we can go back to using AMS’s simple syntax:def show address = Address.find(params[:id]) business = Business.find(params[:business_id]) user = find_user response_object = Api::AddressResponseSerializer::Inputs.new( address, business, user ) render json: response_object, serializer: Api::AddressResponseSerializer end
If the combo of top-level objects becomes a popular one, it may make sense to ditch the
Struct
and move to using a proper class. But for simple responses, this pattern has worked well.When you gotta merge
AMS currently has no nice syntax for merging together the output of multiple serializers. To make this work, you’ll have to do a bit of fiddling under the hood.
Say we’re building a serializer for a
Business
object, and we want to include some address data. There’s already anAddressSerializer
that we’ve been using for this, but in the newBusiness
object, we want to flatten the address object and merge it with the business object. In order to make this work, we need to tap into a method inActiveModel::Serialization
that gets called by AMS,def serializable_hash
.serializable_hash
is the method that returns the serialized Ruby hash of an object’s attributes. In ourBusiness
serializer, we can override that method call, usesuper
to access the original hash, and then merge in our additional address data.def serializable_hash super.merge( Api::AddressSerializer.new(object.address).as_json ) end
One thing to look out for - using
merge
will clobber existing keys with the same name as those being merged in. You may want to usereverse_merge
here instead, depending on your use case. This ‘hack’ isn’t the prettiest thing ever, but I’ve ended up needing it a fair amount to get the required data structure while still keeping things nice and DRY.There are many methods of serializing data in Rails, and no single one is a silver bullet. But hopefully this has given a bit of insight into how to tackle JSON-making in a decent sized Rails app. Good luck, and happy serializing!
-
You've been flagged.
Let’s play a game. I want you to take a look at the following function call and guess what it does:
deleteAllUsers(true);
Do you have it? If you answer was ‘uh, it does nothing’, then you’re correct! See, deleting all the users isn’t so scary. So how does it work its magic?
1 2 3 4 5 6 7 8 9
function deleteAllUsers(inProductionMode) { if (inProductionMode) { return false; } else { fetch("/users", { method: "DELETE" }).then(function() { alert("You have no more users! Time to retire."); }); } }
A-ha, we have ourselves a ‘magic flag’!
A magic flag is an argument whose value changes the behavior of the function being called. These usually show up as booleans, but they can also be numbers, objects, special secret strings… you name it. Things get especially interesting when you combine several different types together. Let’s sprinkle in some defaults too, cuz why not?
1 2 3
def display_contacts(contacts, user_is_mobile, mode = "condensed") # fun stuff end
What does that thing do? What inputs are being passed to it, and why? And what will be returned? Your guess is as good as mine. When you’re reading a method’s code, magic flags make it hard to tell what’s actually going on.
Adding a magic flag usually means that you’re splitting each code path in two, which in the best case means there are now 2n paths through your function. Have three flags? There are now eight different possible outcomes you need to think about. Trying to work with code like that is bad news bears.
It’s also difficult to reason about why the different code paths exist, and under what circumstances they’d be used. Odds are, you’ll probably have to search through the entire codebase to examine every usage for context. And so will everyone else who reads the code. Who doesn’t love a good word search?
Now imagine you’re deep in the wilds of the codebase, and you come across a call to the above method.
contacts_list = display_contacts(user.contacts, true, "full")
That line of code is quite puzzling. Why is there a random boolean argument? What does
"full"
mean? Welp, guess we have to go look it up and find out. And I guarantee you’ll have to keep looking it up every time you come across it. By using magic flags, the author of the function is forcing the person calling it to know about the internal implementation details. Note that this is different than knowing what a function does. We use functions every day without having to read the source code, because we know what they do. That’s how well-written code works - you shouldn’t have to worry about the implementation details in most cases. But when you start adding in flags, you almost always need to peek behind the curtain to see what voodoo magic is going on. It’s a bit personal, don’t you think?How does code like this come about? And what can be done about it? In most cases, I’ve found that magic flags creep in after the original function has been written. Often someone will need it to do a slightly different thing, and rather than writing a separate function, they add in a flag to modify the behavior.
Luckily, this is usually a pretty straight-forward refactor, and can often be solved using function composition. Passing in a flag to a function means that there are now two different code paths to travel down, usually a base case and some optional tacked-on behavior. The base case can be separated out into its own function, and the optional behavior can be a separate function that calls the base function. To illustrate, let’s take a look at an example similar to something I worked on recently.
1 2 3 4 5 6 7 8 9
def item_description(item, is_full_length = true) description = is_full_length ? item.description : item.short_description if item.available? description || "" else Item::DEFAULT_DESCRIPTION[item.status] end end
This method existed originally as just a simple way to show either an item’s description or some default text. As the codebase grew, item descriptions were needed in other places as well, but not always the ‘full’ version. So a magic flag was added to change the behavior. UI code changes every so often, so what happens when someone needs a slightly different version in a few months? Will this grow into
def item_description(item, is_full_length, mode, device = "tablet")
Hell no! Not on my watch. Let’s start by teasing out the base conditional:
1 2 3 4 5 6 7
def item_description_with_default(item, description) if item.available? description || "" else Item::DEFAULT_DESCRIPTION[item.status] end end
Now we have a foundation to build on. Instead of using a flag to alter behavior, let’s compose two new methods that call the
item_description_with_default
method.1 2 3 4 5 6 7
def item_description(item) item_description_with_default(item, item.description) end def short_item_description(item) item_description_with_default(item, item.short_description) end
Yah, naming is hard. But now adding new variations is easy! No magic flags needed. The base behavior is still shared, and we can easily compose new methods to get the output we need. The arguments make sense to the reader, and they don’t need to bother with implementation details. Problem solved!
Granted, things aren’t usually this simple, and searching through a codebase for 30 slightly different calls to the same function will make you reconsider your life choices. But over time, removing magic flags like this will make things a whole lot easier.