10

I am trying to create a group of map markers with the results of to_gmaps4rails in an each block. On an array with valid geo coordinates the to_gmaps4rails method produces valid JSON.

I'm using Mongoid and my geo coordinates are in a sub-collection like so:

Account.locations.coordinates  

Here is my controller code. nearby_sales is a collection of Accounts:

@json = String.new
nearby_sales.each do |sale|
  @json << sale.locations.to_gmaps4rails
end

The browser complains about my @json not being well-formed. Is there a Ruby way to append valid JSON together?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
SteveO7
  • 2,430
  • 3
  • 32
  • 40
  • 2
    Wouldn't you want an array? Otherwise it'd just be json objects in a row with no commas or surrounding structure. – Dave Newton Dec 21 '12 at 12:52

3 Answers3

18

You can't concatenate JSON formatted strings returned by to_gmaps4rails because they won't result in a valid object once decoded.

If I have some objects I want to send:

loc1 = {"longitude" => "2.13012", "latitude" => "48.8014"}
loc2 = {"longitude" => "-90.556", "latitude" => "41.0634"}

And convert them to JSON like to_gmaps4rails does:

loc1_json = loc1.to_json
=> "{\"longitude\":\"2.13012\",\"latitude\":\"48.8014\"}"
loc2_json = loc2.to_json
=> "{\"longitude\":\"-90.556\",\"latitude\":\"41.0634\"}"

They're two JSON-encoded objects as strings.

Concatenate the resulting strings:

loc1_json + loc2_json
=> "{\"longitude\":\"2.13012\",\"latitude\":\"48.8014\"}{\"longitude\":\"-90.556\",\"latitude\":\"41.0634\"}"

And send them to another app with a JSON decoder, I'll get:

JSON[loc1_json + loc2_json]
JSON::ParserError: 743: unexpected token at '{"longitude":"-90.556","latitude":"41.0634"}'

The parser only makes it through the string partway before it finds a closing delimiter and knows there's an error.

I can wrap them in an array or a hash, and then encode to JSON again, but that doesn't help because the individual JSON strings will have been encoded twice, and will need to be decoded twice again to get back the original data:

JSON[([loc1_json, loc2_json]).to_json]
=> ["{\"longitude\":\"2.13012\",\"latitude\":\"48.8014\"}",
    "{\"longitude\":\"-90.556\",\"latitude\":\"41.0634\"}"]

JSON[([loc1_json, loc2_json]).to_json].map{ |s| JSON[s] }
=> [{"longitude"=>"2.13012", "latitude"=>"48.8014"},
    {"longitude"=>"-90.556", "latitude"=>"41.0634"}]

It's not a situation a JSON decoder expects, so that'd require some funky JavaScript on the client side to use the magic JSON decoder-ring twice.

The real solution is to decode them back to their native Ruby objects first, then re-encode them into the array or hash, then send them:

array_of_json = [loc1_json, loc2_json].map{ |s| JSON[s] }.to_json
=> "[{\"longitude\":\"2.13012\",\"latitude\":\"48.8014\"},{\"longitude\":\"-90.556\",\"latitude\":\"41.0634\"}]"

The values are correctly encoded now and can be sent to the destination browser or app, which can then make sense of the resulting data, not as an array of strings as above, but as an array of hashes of data:

JSON[array_of_json]
=> [{"longitude"=>"2.13012", "latitude"=>"48.8014"},
    {"longitude"=>"-90.556", "latitude"=>"41.0634"}]

loc1 == JSON[array_of_json][0]
=> true
loc2 == JSON[array_of_json][1]
=> true

Applying that to your code, here's what needs to be done:

@json = []
nearby_sales.each do |sale|
  @json << JSON[sale.locations.to_gmaps4rails]
end
@json.to_json

This decodes the locations back to their "pre-JSON" state, appends them to the array, then returns the array in JSON format.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • Worked perfectly! Thank you for the working code but especially for the great explanation! – SteveO7 Dec 22 '12 at 17:32
  • One issue I hope you can help me with. Although the json is now well formed, it contains arrays of arrays of hashes. Like [[{"lat":39,"lng":-104},{"lat":39,"lng":-105}],[{"lat":39,"lng":-104.5]] creating one level too deep. Is there a way to append raw hashes to @json inside the block? – SteveO7 Dec 24 '12 at 15:11
  • Wait wait... I think I got it. I added .flatten! to the final line like so; @json.flatten!.to_json and the json returned to the browser looks like I expect it to. I saw some warnings about using flatten can cause unexpected results, any advice on that? – SteveO7 Dec 24 '12 at 15:51
  • You're doing something different than the solution if you're having to do an additional `flatten`, most likely using the wrong operator. I suspect you're doing something like `foo = []; foo << ['bar']` instead of `foo + ['bar']`. Array concatenation, done wrong, will force you to use `flatten` as a work-around. – the Tin Man Dec 24 '12 at 16:09
  • Also, `@json.flatten!.to_json` is the mutating form of `flatten` and not correct for what you are doing. Use `@json.flatten.to_json` if you have to `flatten`. Read up on the difference between the two and understand why you'd use one over the other. – the Tin Man Dec 24 '12 at 16:12
3
require 'json'

@json = Array.new
nearby_sales.each do |sale|
  @json << sale.locations.to_gmaps4rails
end
@json.to_json
0

You can always try this

array_of_json.reduce({}){|z,x| z.merge(x)}
Maysam Torabi
  • 3,672
  • 2
  • 28
  • 31