Ruby, Serialization, and Enumeration

In which a curious backtrace uncovers scandalous truths about ActiveSupport and Ruby’s IO object.

Square Engineering
Square Corner Blog
3 min readMay 25, 2014

--

Written by Xavier Shay.

Heads up, we’ve moved! If you’d like to continue keeping up with the latest technical content from Square please visit us at our new home https://developer.squareup.com/blog

We came across an interesting Ruby puzzler the other day. Code that had been working fine had started failing during serialization with a scary looking backtrace from within activesupport:

The issue was isolated to a particular change to a class that was being serialized inside a larger data structure (which I’ve further simplified):

The addition of the @logger variable was causing the problem, even though it was never used! Commenting out that line made the problem go away. This is certainly non-intuitive. What is going on? Let’s find out.

One of Ruby’s best features is the transparency of its libraries. We have the full path to the source of the exception right there in the backtrace. Opening up /gems/activesupport-4.1.1/lib/active_support/core_ext/object/json.rbin your favourite editor (obviously your path will be different) gives a major hint: this file is monkey-patching a number of core classes to support serialization.

Looking at the Metric code above, how did it know how to serialize the object to JSON in the first place? Sure enough, elsewhere in json.rb (which also appears in our stack trace):

Metric does not implement to_hash, so all of its instance variables are being included in the JSON representation. That explains why the unused @logger variable was the difference between success and failure. An easy fix then is to implement to_hash. Even if @logger was serializing correctly, it is unwanted in the JSON output.

However that still doesn’t explain the opaque error: not opened for reading (IOError). The quickest way to track that down, since we already have json.rbopen, is to add some quick debugging output before the line that failed:

This is why library transparency in Ruby is particularly amazing: you can also modify them in-place and see the results immediately!

In this instance a single line is output:

That explains it. For better or worse, IO objects in Ruby are enumerable. The semantics here are questionable: to_a is indeed part of the Enumerable interface, but making an IO into an array doesn’t really make sense. And having each chunk on line breaks is somewhat arbitrary anyway.

This mismatch has historically dogged Enumerable and uses of that interface. Take this gem from rspec-expectations:

The comment says it all really! Funnily enough, the caller of this method has to special case IO as well:

String is no longer Enumerable since Ruby 1.9. Should IO follow in its footsteps?

--

--