It’s fairly simple to extract the data from a JSONP callback in a reliable, repeatable way once you know how. Here’s how.

I wanted to extract the playlist from an internet radio station. The server in question exposes currently playing information via a JSONP API, the intended purpose of which is to update an HTML element on a page with the details of the track currently playing.

JSONP, in case you’re not familiar with the concept, is a means of inserting data from a third-party service into a web page. The third-party server responds to a request with a fragment of JavaScript that calls a function in the originating page.

The service in question returns data like this:

var arr14b178d7 = { 'song': 'blah', 'listeners': 7 };

cc_streaminfo_get_callback(arr14b178d7)

How do we get this into a Ruby application? We could attempt to parse the JavaScript object itself, but that’s a bit ugly and error-prone. We can’t use a JSON parser, because it doesn’t conform to the (restrictive) rules of JSON. Much better to use something like Johnson to evaluate it.

Unfortunately, that doesn’t work all that well:

Johnson.evaluate(s)

# => Johnson::Error: cc_streaminfo_get_callback is not defined at (none):3

Well, let’s split on the blank line, then, and just evaluate the object:

Johnson.evaluate(s.split(/\n\n/).first)

# => nil

Still no good: variable assignment doesn’t return a value, so all we get is nil. And besides, that was a rather fragile attempt at parsing.

In fact, we can take advantage of the fact that the function call is the last statement in the JSONP. If we define the missing function as a stub that returns its argument, then that will be the final result of the JavaScript fragment:

callback = <<-END
  function cc_streaminfo_get_callback(details){
    return details;
  }
END
object = Johnson.evaluate(callback + s)

# => [object Object]

And that object is the thing we want:

object["song"]
# => "blah"

And there you go: a simple and reasonably reliable way of parsing the response from a JSONP API.

(Strictly speaking, I’m not sure if this API actually counts as JSONP as it doesn’t allow us to specify the function name. That doesn’t make any difference to the implementation.)