Allow encoding of already encoded JSON strings..
Opened this issue · 6 comments
Imagine you have a string, that already has JSON.. but you'd like to wrap that JSON object in an array with others. You may have gotten that JSON object from a cache, or maybe its a Javascript function in a string (like a Handlebars pre-compiled template). It would be nice to effectively "join" JSON objects during encoding. This is a nice optimization, and also convenient. Below is an example and idea on how to implement this.
# One idea to implement this is....
# Get an array of json-encoded strings with ids 1, 2, 3, and 4
x = JsonCache.get([1,2,3,4]).map {|o| Yajl::JsonString.new(o) }
# x[0] = "{ id: 1, name: John}"
# x[1] = "{ id: 2, name: Peter}"
# x[2] = "{ id: 3, name: Mike}"
# x[3] = "{ id: 4, name: Allan}"
json = Yajl::Encoder.encode(x)
# json should look like:
# "[{ id: 1, name: John}, { id: 2, name: Peter}, { id: 3, name: Mike}, { id: 4, name: Allan}]"
# currently Yajl will encode each string and instead will return:
# "[\"{ id: 1, name: John}\", \"{ id: 2, name: Peter}\", \"{ id: 3, name: Mike}\", \"{ id: 4, name: Allan}\"]"
Of course you could just decode each object in x, put it in an array, and then re-encode. Fine. But thats unnecessary, and here's a better example that can't be achieved unless the encoder understands strings as native JSON types to just concatenate them to the result.
# Pseudo class..
compiled_template = HandlebarsCompiler.compile("hi {{var}}")
# x
# => "function (Handlebars,depth0,helpers,partials,data) {\n helpers = helpers || Handlebars.helpers;\n var buffer = \"\", stack1, foundHelper, self=this, functionType=\"function\", helperMissing=helpers.helperMissing, undef=void 0, escapeExpression=this.escapeExpression;\n\n\n buffer += \"hi \";\n foundHelper = helpers['var'];\n stack1 = foundHelper || depth0['var'];\n if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, { hash: {} }); }\n else if(stack1=== undef) { stack1 = helperMissing.call(depth0, \"var\", { hash: {} }); }\n buffer += escapeExpression(stack1);\n return buffer;}"
template = {
:name => Yajl::JsonString.new(compiled_template)
}
puts Yajl::Encoder.encode(template)
# The idea is the encoder would return: "{ name: function (Handlebars,depth0,helpers,partials,data) { ..etc. etc. } }"
Now, when parsed by the browser, it doesn't have the eval() the function.
I just came up with the Yajl::JsonString class, which would inherit from a String, and add a method called "is_json" set to true. This way when the encoder is traversing strings, it can test for is_json, and just concatenate instead of encode.
What do you think?
Interesting idea, I'll see if I can explore this a little more in my yajl-ruby 2.0 stuff (there's a branch going already). For now, something like this should work in place of Yajl::JsonString
in your example:
class JsonWrapper
def initialize(json_string)
@json_string = json_string
end
# yajl-ruby will check if this method exists and call it if so
# then append the return value directly onto the output buffer as-is
# this means that this method is assumed to be returning valid JSON
def to_json
@json_string
end
end
But, let me know if otherwise ;)
Thanks. The JsonWrapper worked. I tried to write a JsonString class using your example as so:
class JsonString < String
def initialize(json_string)
@json_string = json_string
super(json_string)
end
def to_json
@json_string
end
end
However, this didn't seem to work, the encoder still encoded the string. How come? I see in yajl_ext.c that a to_json method is being defined for Strings.. and interestingly, the to_json method in JsonString isn't being called.. it's calling the to_json in String.
Either way, the JsonWrapper will get me by, thanks!
Ah - this is because yajl-ruby will (for the sake of efficiency) handle encoding anything that directly translates to a native JSON type (string, number, float, true, false, nil, array and hash) directly down in C.
Damn, didn't mean submit that yet...
Anyway, in those cases I don't check for to_json
being defined. The to_json
check is more of a fallback in case the object being encoded isn't one of those natively translatable types (like an ActiveRecord::Base
instance for example). If the object doesn't response to to_json
either, then I finally fall back to just calling to_s
.
Any progress on that? My use case is encoding quite big arrays of complex objects and right now I ended up with cached objects as json string and creating the final json "by hand" just joining strings etc. I'd love to see yajl encoder that could handle that.
Simple wrapper works just fine - see http://stackoverflow.com/questions/15280117/composing-json-from-cached-strings-in-ruby/16180325#16180325.