I am parsing JSON from an API with the following code:
def json_test
content = open("API URL").read
json = JSON.parse(content)
json.each do |a|
puts a["first_name"]
end
end
The reason I'm using each is because the API request will return an array of hashes for multiple users, like this:
[{
"id": "1",
"first_name": "John"
},
{
"id": "2",
"first_name": "Bob"
}]
However, the API will return just a hash if the request only returns a single user, like thus:
{
"id": "2",
"first_name": "Bob"
}
This is where I'm getting the error message: can't convert String into Integer (TypeError)
I've been searching for a good way to be able to parse when it's not returning an array but just a hash and I'm stumped. Any pointers?
Array.wrap is designed just for this purpose.
Wraps its argument in an array unless it is already an array (or array-like).
Array.wrap({ a: 1 }) # => [{ a: 1 }]
Array.wrap [{ a: 1 }, { b: 2 }] # => [{ a: 1 }, { b: 2 }]
One way is putting it all inside a single element array and flattening it:
json = [JSON.parse(content)].flatten
If it is not an array, the flatten will be noop. If it is, the extra array layer will be removed.
Related
I'm parsing a JSON result into a Ruby hash. The JSON result looks like this:
{
"records": [
{
"recordName": "7DBC4FAD-D18C-476A-89FB-14A515098F34",
"recordType": "Media",
"fields": {
"data": {
"value": {
"fileChecksum": "ABCDEFGHIJ",
"size": 9633842,
"downloadURL": "https://cvws.icloud-content.com/B/ABCDEF"
},
"type": "ASSETID"
}
},
"recordChangeTag": "ii23box2",
"created": {
"timestamp": 1449863552482,
"userRecordName": "_abcdef",
"deviceID": "12345"
},
"modified": {
"timestamp": 1449863552482,
"userRecordName": "_abcdef",
"deviceID": "12345"
}
}
]
}
I can't guarantee that it'll return with any/all those values, or that each value will be of a certain type (e.g. Array, Hash, string, number), and if I call it incorrectly then I get a crash.
Right now I need the downloadURL for the first item in the 'records' array, or to write it as I might with the Swift library SwiftyJSON (which I'm far more familiar with):
json["records"][0]["fields"]["data"]["value"]["downloadURL"]
I'm wondering what the safest/best/standard way to do this safely in Ruby is. Perhaps I'm thinking about it wrong?
In ruby 2.3 and above you can use Hash#dig and Array#dig
json = JSON.parse(...)
json.dig('records', 0, 'fields', 'data', 'value', 'downloadURL')
You'll get nil if any of the intermediate values is nil. If one of the intermediate values doesn't have a dig method, for example if `json['records'][0]['fields'] was unexpectedly an integer this will raise TypeError.
From the documentation (http://ruby-doc.org/stdlib-2.2.3/libdoc/json/rdoc/JSON.html):
require 'json'
my_hash = JSON.parse('{"hello": "goodbye"}')
puts my_hash["hello"] => "goodbye"
If you're worried that you might not have some data. See this question:
Equivalent of .try() for a hash to avoid "undefined method" errors on nil?
You can recursively search each object contained in the json object using
the recurse_proc method of the JSON module.
Here is an example using the data you provided.
require 'json'
json_string = '{
"records": [
{
"recordName": "7DBC4FAD-D18C-476A-89FB-14A515098F34",
"recordType": "Media",
"fields": {
"data": {
"value": {
"fileChecksum": "ABCDEFGHIJ",
"size": 9633842,
"downloadURL": "https://cvws.icloud-content.com/B/ABCDEF"
},
"type": "ASSETID"
}
},
"recordChangeTag": "ii23box2",
"created": {
"timestamp": 1449863552482,
"userRecordName": "_abcdef",
"deviceID": "12345"
},
"modified": {
"timestamp": 1449863552482,
"userRecordName": "_abcdef",
"deviceID": "12345"
}
}
]
}'
json_obj = JSON.parse(json_string)
JSON.recurse_proc(json_obj) do |obj|
if obj.is_a?(Hash) && obj['downloadURL']
puts obj['downloadURL']
end
end
Update Based on Frederick's answer and Cary's comment
I originally assumed you just wanted to find the downloadURL somewhere in the json without crashing, but based on Frederick's answer and Cary's comment, it's reasonable to assume that you only want to find the downloadURL if it is at the exact path, rather than if it just exists. Building on Frederick's answer and Cary's comment here are a couple of other options that should safely find the downloadURL at the expected path.
path = ['records', 0, 'fields', 'data', 'value', 'downloadURL']
parsed_json_obj = JSON.parse(json_string)
node_value = path.reduce(parsed_json_obj) do |json,node|
if json.is_a?(Hash) || (json.is_a?(Array) && node.is_a?(Integer))
path = path.drop 1
json[node]
else
node unless node == path.last
end
end
puts node_value || "not_found"
path = ['records', 0, 'fields', 'data', 'value', 'downloadURL']
begin
node_value = parsed_json_obj.dig(*path)
rescue TypeError
node_value = "not_found"
end
puts node_value || "not_found"
BTW, this assumes the json is at least valid, if that is not a given you might want to wrap the JSON.parse in a begin-rescue-end block as well.
I currently have a nested JSON object which resembles
{
"People": [
{
"Name": "James",
"Age": "18",
"Gender": "Male",
"Sports": []
},
{
"Name": "Sarah",
"Age": "19",
"Gender": "Female",
"Sports": [
"Soccer",
"Basketball",
"Football"
]
}
]
}
Being new to Ruby, I aim to filter throught the entire json and return only the json object/objects in which the "Sports" array has content. So in the above scenario I expect to obtain the object below as a final outcome:
{
"Name": "Sarah",
"Age": "19",
"Gender": "Female",
"Sports": [
"Soccer",
"Basketball",
"Football"
]
}
Will I have to initiate a new method to perform such an act? Or would using regular ruby calls work in this case?
Although #philipyoo answer is right, it miss an explanation on how to "filter" the parsed JSON. If you are new to ruby, take a look at Array#keep_if : http://ruby-doc.org/core-2.2.0/Array.html#method-i-keep_if
require 'json'
people = JSON.parse("{long JSON data ... }")
people_with_sports = people.fetch('People', []).keep_if do |person|
!person.fetch('Sports', []).empty?
end
If you're getting a JSON object from a request, you want to parse it and then you can traverse the hash and arrays to find the information you need. See http://ruby-doc.org/stdlib-2.0.0/libdoc/json/rdoc/JSON.html
In your case, something like this:
require 'json'
parsed_json = JSON.parse('{"People": [ ... ]}')
parsed_json["People"].each do |person|
puts person if person["name"] == "Sarah"
end
I have this JSON and I am trying to send it to a Rails API from Postman:
{"object":
{
"type": "out",
"vars":
{
"x": "x",
"y": "y"
},
"values":
{
"ts": "timestamp",
"ok":
{
"total": 2,
"min": "x",
"max": "y"
},
"error":
{
"total": 2,
"error1": "first",
"error2": "second"
}
}
}
}
I need to convert this into a Hash in my model so that I can manipulate it with before_create. Here's what I came with:
object = self.to_json # => converts object to json
object = JSON.parse(object) # => converts json to hash
1st problem: I get this (id=>nil is not relevant since it will be inserted automatically in the database):
{"id"=>nil, "type"=>"out", "vars"=>{"x"=>"x", "y"=>"y"}, "values"=>{"ts"=>"timestamp", "ok"=>"{\"total\"=>2, \"min\"=>\"x\", \"max\"=>\"y\"}", "error"=>"{\"total\"=>2, \"error1\"=>\"first\", \"error2\"=>\"second\"}"}, "created_at"=>"2015-01-29T15:45:01.329Z", "updated_at"=>"2015-01-29T15:45:01.329Z"}
and when I try to manipulate object["values"]["ok"], Rails sends the error:
unexpected token at '"{\"total\"=\u003e2, \"min\"=\u003e\"x\", \"max\"=\u003e\"y\"}"'
2nd problem: I can only call object["values"], and I want to call it with a symbol, not a string object[:values].
Solved my issues using:
object = self.as_json.with_indifferent_access
# => allowing me to use a symbol key instead of a string
ok_vals = object[:values][:ok].as_json.gsub(/\=\>/, ':')
# => allowing to change json string '{"val1"=>"val1", "val2"=>"val2"}' to '{"val1":"val1", "val2":"val2"}'
ok_vals = JSON.parse(ok_vals)
# => which transform json string to hash {val1: "val1", val2: "val2"}
Feel free to make any suggestions to this code. Thanks for the help.
I have the following JSON string returned by a remote server:
{
"users": [
{
"user_id": 1,
"name": "Chris Rivers",
},
{
"user_id": 3,
"name": "Peter Curley",
}
]
}
I'd like to iterate the users.
ActiveSupport::JSON.decode(response.body)["users"].each do |user|
puts user["name"]
end
As far as I understand, the problem is: ruby doesn't recognize ActiveSupport::JSON.decode(response.body)["users"] as an array, and thus puts returns me only the first user.
How do I solve that problem?
What you have pasted is not valid JSON. The trailing comma after on each "name" is a problem
"name": "Chris Rivers",
You'll get a LoadError trying to decode this with ActiveSupport::JSON.decode
MultiJson::LoadError: 399: unexpected token at '{"user_id": 1,"name": "Chris Rivers",},{"user_id": 3,"name": "Peter Curley",}]}'
If we clean up the JSON, turning it into something ActiveSupport::JSON.decode can understand
"{\"users\": [{\"user_id\": 1,\"name\": \"Chris Rivers\"},{\"user_id\": 3,\"name\": \"Peter Curley\"}]}"
you'll see there is no issue iterating over each object in "users" (x below is the above JSON string)
[8] pry(main)> ActiveSupport::JSON.decode(x)["users"].map { |user| user["name"] }
=> ["Chris Rivers", "Peter Curley"]
Does your source data actually have the trailing commas after each user's name? I get a parse error for that, but your code works as you want it to if I remove them:
json = '{ "users": [ { "user_id": 1, "name": "Chris Rivers" }, { "user_id": 3, "name": "Peter Curley" } ]}'
ActiveSupport::JSON.decode(json)["users"].each do |user|
puts user["name"]
end
The problem isn't not recognizing the array, it's the trailing commas after the "name" elements.
Removing those allows JSON parsing to proceed normally:
pry(main)> ActiveSupport::JSON.decode(s)["users"]
=> [{"user_id" => 1, "name" => "Chris Rivers"},
{"user_id" => 3, "name" => "Peter Curley"}]
The default way to output JSON in rails is some thing like:
Code:
render :json => friends.to_json(:only => [:username, :avatar_file_name, :id ])
Output
{"friends" :
[{"user":
{"avatar_file_name": "image1.jpg", "username": "user1", "id": 1}},
{"user":
{"avatar_file_name": "image2.jpg", "username": "user2", "id": 2}},
{"user":
{"avatar_file_name": "image3.jpg", "username": "user3", "id": 3}}
]}
But i want something like:
{"friends" :
{"user": [
{"avatar_file_name": "image1.jpg", "username": "user1", "id": 1},
{"avatar_file_name": "image2.jpg", "username": "user2", "id": 2},
{"avatar_file_name": "image3.jpg", "username": "user3", "id": 3}
]}
}
The class is specified by the array name.
Last.fm also uses this syntax see Last.fm 'API-user.getfriends'
The solution to this problem is commenting the line
ActiveRecord::Base.include_root_in_json = true
in initializers/new_rails_defaults.rb
Or setting ActiveRecord::Base.include_root_in_json to false.
You can use javascript to reformat it:
var json =
{
"friends" :
{ "user": [] }
}
var i = 0;
for ( x in friends )
{
json.friends.user[i].avatar_file_name = x.user.avatar_file_name; // add more fields.
i++;
}
Something among those lines.
JSON is normally used to represent objects in a text format.
So if you like the secon output you must change your objects.
The first output says:
there is a friends object which is a array of user, each user has some properties among which you chose to expose username, avatar_file_name, id
The second output says:
there is a friends object which contains a user object which is an array of unnamed objects, each unnamed objects has some properties...
This second output is not writable in JSON syntax.
It might be:
{"friends" :
{"user": [
["avatar_file_name", "username", "id"],
["image1.jpg", "user1", 1],
["image2.jpg", "user2", 2],
["image3.jpg", "user3", 3]
]}
}
This says:
there is a friends object which contains a user object which is an array of array (a table with field names on first row) ...