I looked into different resources and still get confused on how to parse a json format to a custom object, for example
class Resident
attr_accessor :phone, :addr
def initialize(phone, addr)
#phone = phone
#addr = addr
end
end
and JSON file
{
"Resident": [
{
"phone": "12345",
"addr": "xxxxx"
}, {
"phone": "12345",
"addr": "xxxxx"
}, {
"phone": "12345",
"addr": "xxxxx"
}
]
}
what's the correct way to parse the json file into a array of 3 Resident object?
Today i was looking for something that converts json to an object, and this works like a charm:
person = JSON.parse(json_string, object_class: OpenStruct)
This way you could do person.education.school or person[0].education.school if the response is an array
I'm leaving it here because might be useful for someone
The following code is more simple:
require 'json'
data = JSON.parse(json_data)
residents = data['Resident'].map { |rd| Resident.new(rd['phone'], rd['addr']) }
If you're using ActiveModel::Serializers::JSON you can just call from_json(json) and your object will be mapped with those values.
class Person
include ActiveModel::Serializers::JSON
attr_accessor :name, :age, :awesome
def attributes=(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
def attributes
instance_values
end
end
json = {name: 'bob', age: 22, awesome: true}.to_json
person = Person.new
person.from_json(json) # => #<Person:0x007fec5e7a0088 #age=22, #awesome=true, #name="bob">
person.name # => "bob"
person.age # => 22
person.awesome # => true
require 'json'
class Resident
attr_accessor :phone, :addr
def initialize(phone, addr)
#phone = phone
#addr = addr
end
end
s = '{"Resident":[{"phone":"12345","addr":"xxxxx"},{"phone":"12345","addr":"xxxxx"},{"phone":"12345","addr":"xxxxx"}]}'
j = JSON.parse(s)
objects = j['Resident'].inject([]) { |o,d| o << Resident.new( d['phone'], d['addr'] ) }
p objects[0].phone
"12345"
We recently released a Ruby library static_struct that solves the issue. Check it out.
Related
Let's say I want to set up a validation contract for addresses, but then I also want to set up a validator for users, and for coffee shops; both of which include an address, is it possible to re-use the AddressContract in UserContract and CoffeeShopContract?
For example, the data I want to validate might look like:
# Address
{
"first_line": "100 Main street",
"zipcode": "12345",
}
# User
{
"first_name": "Joe",
"last_name": "Bloggs",
"address:" {
"first_line": "123 Boulevard",
"zipcode": "12346",
}
}
# Coffee Shop
{
"shop": "Central Perk",
"floor_space": "2000sqm",
"address:" {
"first_line": "126 Boulevard",
"zipcode": "12347",
}
}
Yes you can reuse schemas (See: Reusing Schemas)
It would look something like this:
require 'dry/validation'
class AddressContract < Dry::Validation::Contract
params do
required(:first_line).value(:string)
required(:zipcode).value(:string)
end
end
class UserContract < Dry::Validation::Contract
params do
required(:first_name).value(:string)
required(:last_name).value(:string)
required(:address).schema(AddressContract.schema)
end
end
a = {first_line: '123 Street Rd'}
u = {first_name: 'engineers', last_name: 'mnky', address: a }
AddressContract.new.(a)
#=> #<Dry::Validation::Result{:first_line=>"123 Street Rd"} errors={:zipcode=>["is missing"]}>
UserContract.new.(u)
#=> #<Dry::Validation::Result{:first_name=>"engineers", :last_name=>"mnky", :address=>{:first_line=>"123 Street Rd"}} errors={:address=>{:zipcode=>["is missing"]}}>
Alternatively you can create schema mixins as well e.g.
AddressSchema = Dry::Schema.Params do
required(:first_line).value(:string)
required(:zipcode).value(:string)
end
class AddressContract < Dry::Validation::Contract
params(AddressSchema)
end
class UserContract < Dry::Validation::Contract
params do
required(:first_name).value(:string)
required(:last_name).value(:string)
required(:address).schema(AddressSchema)
end
end
Trying to build a json out of Ruby hash
require 'json'
temp_rides = {"rides": {}}
rides = {:lyft => "car", :scoot => "scooter", :blade => "helicopter"}
rides.each do |key, value|
temp_rides["rides"].push({"key" => "value"})
puts temp_rides
end
apparently it fails with undefined methodpush'` I believe I need to load the json object before append (based on my python background).
I am looking for a output something like this
{
"rides": {
"lyft": {
"type": "car"
},
"scoot": {
"type": "scooter"
},
"blade": {
"type": "helicopter"
}
}
}
This is an answer to the OP's original question. I have no interest in changing it to attempt to answer a moving target.
Code
def doit(rides)
{ rides: rides.slice_before { |k,_| k.to_s.match? /\Aride\d*\z/ }.
map { |a| hashify(a.flatten.drop(1)) }.
reduce(&:merge) }
end
def hashify(arr)
f,*rest = arr
return f if rest.empty?
{ f.to_sym=>hashify(rest) }
end
Examples
Here is an example of the use of the (recursive) helper method hashify:
hashify ["lyft", :type, "car"]
#=> {:lyft=>{:type=>"car"}}
We are given the hash rides:
rides = {:ride1=>"lyft", :type=>"car", :ride2=>"Scoot",
:type2=>"scooter", :ride3=>"blade", :type3=>"helicopter"}
doit rides
#=> {:rides=>{:lyft=>{:type=>"car"},
# :Scoot=>{:type2=>"scooter"},
# :blade=>{:type3=>"helicopter"}}}
Let's add some more key-value pairs to rides:
rides = {:ride1=>"lyft", :type=>"car", :color=>"blue",
:ride2=>"Scoot", :type2=>"scooter", :make=>"Vespa", :model=>"98",
:ride3=>"blade", :type3=>"helicopter"}
doit rides
#=> {:rides=>{:lyft=>{:type=>{:car=>{:color=>"blue"}}},
# :Scoot=>{:type2=>{:scooter=>{:make=>
# {:Vespa=>{:model=>"98"}}}}},
# :blade=>{:type3=>"helicopter"}}}
Explanation
The steps for the first example are as follows.
enum = rides.slice_before { |k,_| k.to_s.match? /\Aride\d*\z/ }
#=> #<Enumerator: #<Enumerator::Generator:0x00005a49d68217f0>:each>
We can see the elements that will be generated by this enumerator by converting it to an array.
enum.to_a
#=> [[[:ride1, "lyft"], [:type, "car"]],
# [[:ride2, "Scoot"], [:type2, "scooter"]],
# [[:ride3, "blade"], [:type3, "helicopter"]]]
Continuing,
a = enum.map { |a| hashify(a.flatten.drop(1)) }
#=> [{:lyft=>{:type=>"car"}},
# {:Scoot=>{:type2=>"scooter"}},
# {:blade=>{:type3=>"helicopter"}}]
h = a.reduce(&:merge)
#=> {:lyft=>{:type=>"car"}, :Scoot=>{:type2=>"scooter"},
# :blade=>{:type3=>"helicopter"}}
{ rides: h }
#=> <as above>
It gave you undefined method push because there is no such method for a hash.
temp_rides = {"rides": {}}
# This create a hash with `:rides` symbol as the key {:rides=>{}}
# To push an object into a hash. Use operator[]= or #store method
temp_rides[:rides][:key1] = 'value1'
temp_rides[:rides].store(:key2, 'value2')
A working example:
require 'json'
temp_rides = {"rides": {}}
rides = {:lyft => "car", :scoot => "scooter", :blade => "helicopter"}
rides.each_pair do |k, v|
temp_rides[:rides][k] = {:type => v}
end
puts JSON.pretty_generate(temp_rides)
This is actually pretty easy to do with a simple transform:
def rejig(rides)
rides.map do |name, type|
[ name, { type: type } ]
end.to_h
end
Where that will rework your structure into the desired shape.
Using it is easy:
require 'json'
rides = { lyft: "car", scoot: "scooter", blade: "helicopter" }
puts JSON.dump(rejig(rides))
# => {"lyft":{"type":"car"},"scoot":{"type":"scooter"},"blade":{"type":"helicopter"}}
require 'json'
temp_rides = {"rides": {}}
rides = {:lyft => "car", :scoot => "scooter", :blade => "helicopter"}
temp_rides[:rides].merge!(rides)
to get ruby hash output
temp_rides
to get json format output
JSON.dump temp_rides
require 'json'
rides = {:lyft => "car", :scoot => "scooter", :blade => "helicopter"}
transformed = {
'rides' => rides.inject({}) { |h,(k,v)| h[k] = { 'type' => v }; h }
}
JSON.dump(transformed)
Is there any better way to do the below code?
user.name = "abc"
user.email = "abc#test.com"
user.mobile = "12312312"
Something like this will do:
user.prepare do |u|
u.name = "abc"
u.email = "abc#test.com"
u.mobile = "12312312"
end
tap let's you do exactly that:
user.tap do |u|
u.name = "abc"
u.email = "abc#test.com"
u.mobile = "12312312"
end
Alternative option when your attributes come in the form of a hash:
attrs = {
name: "abc",
email: "abc#test.com",
mobile: "12312312"
}
attrs.each { |key, value| user.send("#{key}=", value) }
You could do the following as well:
user.instance_eval do
#name = "abc"
#email = "abc#test.com"
#mobile = "12312312"
end
You can access the instance variables of user inside the block given to instance_eval
You could use the below code if you wish to invoke the accessor methods instead of directly manipulating the instance variables.
user.instance_eval do
self.name = "xyz"
self.email = "abc#test.com"
self.mobile = "12312312"
end
or
user.instance_eval do |o|
o.name = "xyz"
o.email = "abc#test.com"
o.mobile = "12312312"
end
With ActiveRecord objects you can use .assign_attributes or the update
methods:
user.assign_attributes( name: "abc", email: "abc#test.com", mobile: "12312312")
# attributes= is a shorter alias for assign_attributes
user.attributes = { name: "abc", email: "abc#test.com", mobile: "12312312" }
# this will update the record in the database
user.update( name: "abc", email: "abc#test.com", mobile: "12312312" )
# or with a block
user.update( name: "abc", mobile: "12312312" ) do |u|
u.email = "#{u.name}#test.com"
end
.update accepts a block, while assign_attributes does not. If you are simply assigning a hash of literal values - such as those passed by a user in the params then there is no need to use a block.
If you have a plain old ruby object which you want to spice up with mass assignment you can do:
class User
attr_accessor :name, :email, :mobile
def initialize(params = {}, &block)
self.mass_assign(params) if params
yield self if block_given?
end
def assign_attributes(params = {}, &block)
self.mass_assign(params) if params
yield self if block_given?
end
def attributes=(params)
assign_attributes(params)
end
private
def mass_assign(attrs)
attrs.each do |key, value|
self.public_send("#{key}=", value)
end
end
end
This will let you do:
u = User.new(name: "abc", email: "abc#test.com", mobile: "12312312")
u.attributes = { email: "abc#example.com", name: "joe" }
u.assign_attributes(name: 'bob') do |u|
u.email = "#{u.name}#example.com"
end
# etc.
Assuming that 'user' is a class that you control, then you can just define a method to do what you want. For example:
def set_all(hash)
#name, #email, #mobile = hash[:name], hash[:email], hash[:mobile]
end
And then in the rest of your code:
user.set_all(name: "abc", email: "abc#test.com", mobile: "12312312")
If 'user' is an instance of, say, an ActiveRecord model, then I'm a little shaky on the details of how you would get this to work. But the principal still applies: DRY up code by moving the responsibility for the complexity to the receiver.
In ruby how can i parse a json to an array of objects?
Example: i have 2 classes:
class Person
attr_accessor :name, :address, :email, :address
end
And:
class Address
attr_accessor :street, :city, :state, :person
end
When i make a request i get the following json:
{
"data": [
{
"id": 9111316,
"name": "Mason Lee",
"email": "normanodonnell#biospan.com",
"address": {
"state": "American Samoa",
"street": "Cameron Court",
"city": "Wakulla"
}
},
{
"id": 500019,
"name": "Stella Weeks",
"email": "hansenwhitfield#candecor.com",
"address": {
"state": "Nevada",
"street": "Lake Street",
"city": "Wacissa"
}
}
]
}
This json should be parsed into an array of Person.
For now i'm doing:
#json gem
require 'json'
#...
#parse the json and get the 'data'
parsed_json = JSON.parse json
json_data = parsed_json['data']
objects = Array.new
if json_data.kind_of?(Array)
#add each person
json_data.each { |data|
current_person = Person.new
data.each { |k, v|
current_person.send("#{k}=", v)
}
objects.push(current_person)
}
end
#return the array of Person
objects
I have a lot of objects like the above example and do this parse manually is not desirable. There is an automated way to do this?
By "automated way" i mean something like in java with jackson:
ObjectMapper mapper = new ObjectMapper();
List<Person> myObjects = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, Person.class));
You can initialize the Person with the hash:
json_data = JSON.parse(json)['data']
json_data.map do |data|
Person.new data
end
class Person
attr_accessor :name, :email, :address
def initialize params
params.each { |k,v| klass.public_send("#{k}=",v) }
end
end
If you want to choose the class dynamically, you can use:
json_data.map do |data|
klass = 'Person'
klass.get_const.new data
Why not just make the method yourself? Example:
require 'json'
def parse_json_to_class_array(data,root_node,to_klass)
json_data = JSON.parse(data)[root_node]
if json_data.is_a?(Array)
objects = json_data.map do |item|
klass = to_klass.new
item.each { |k,v| klass.public_send("#{k}=",v) }
klass
end
end
objects ||= []
end
Then for your example you could call it like so
json ="{\"data\":[
{\"id\":9111316,
\"name\":\"Mason Lee\",
\"email\":\"normanodonnell#biospan.com\",
\"address\":{
\"state\":\"American Samoa\",
\"street\":\"Cameron Court\",
\"city\":\"Wakulla\"
}
},
{\"id\":500019,
\"name\":\"Stella Weeks\",
\"email\":\"hansenwhitfield#candecor.com\",
\"address\":{
\"state\":\"Nevada\",
\"street\":\"Lake Street\",
\"city\":\"Wacissa\"
}
}
]
}"
class Person
attr_accessor :id, :name,:email, :address
end
parse_json_to_class_array(json,'data',Person)
#=>[#<Person:0x2ede818 #id=9111316, #name="Mason Lee", #email="normanodonnell#biospan.com", #address={"state"=>"American Samoa", "street"=>"Cameron Court", "city"=>"Wakulla"}>,
#<Person:0x2ede7a0 #id=500019, #name="Stella Weeks", #email="hansenwhitfield#candecor.com", #address={"state"=>"Nevada", "street"=>"Lake Street", "city"=>"Wacissa"}>]
Obviously you can expand this implementation to support single objects as well as overwrite Person#address= to perform the same operation and turn the address Hash into an Address object as well but this was not shown in your example so I did not take it this far in my answer.
A more dynamic example can be found Here
I am trying to create a monopoly manager for fun and I have the data for all the tiles in a JSON file which I'd like to use to iterate over and use for the data. I currently have this code:
require 'json'
file = File.read('monopoly-data.json')
data_hash = JSON.parse(file)
data = data_hash['properties'].sort_by{ |e| e['id'].to_i }
class Property
attr_accessor :id, :group, :colour, :name, :price, :rent, :house_price, :mortage
def initialize(params = {})
#id = params[:id]
#group = params[:group]
#colour = params[:colour]
#name = params[:name]
#price = params[:price]
#rent = params[:rent]
#house_price = params[:house_price]
#mortage = params[:mortage]
end
end
And the JSON file is in this gist.
I haven't been able to figure out how to use the JSON data to make more properties, I have played around with for loops on the data, data.each trying to make something like this automatically:
parkveien = Property.new(
id: 1,
group: 'property',
colour: 'brown',
name: 'Parkveien',
price: 1200,
rent: [40, 200, 600, 1800, 3200, 5000],
house_price: 1000,
mortage: 600
)
However I just can't wrap my head around how to do that, any help would be highly appreciated!
You can use each to generate the Property objects and collect them into an array.
properties = []
data.each { |property| properties << Property.new(property) }
You can then access individual properties from the array by using select as needed.
properties.select { |property| property.name == "Parkveien" }
Also, since the keys in your data hash are strings, you need to change your initialize to use strings instead of symbols:
def initialize(params = {})
#id = params["id"]
#group = params["group"]
#colour = params["colour"]
#name = params["name"]
#price = params["price"]
#rent = params["rent"]
#house_price = params["house_price"]
#mortage = params["mortage"]
end