Search code examples
ruby-on-railsjsonrubypostgresqljsonpath

Ruby on Rails: How can I use JSONPath to access (and save to database) nested objects/properties within a JSON array?


I am trying to seed data to my database from a JSON array of objects. I have two separate database tables - Property and Unit (a Property has many Units). I have been able to do this for the property information (property model) successfully with the API requesting the JSON data and then seeding this to the database however the ValuationReport attribute is showing on my front end as a JSON object in string form.

Here is a sample of the JSON data I am working with (I've limited this sample to an array of 3 objects for demonstration purposes):

[
  {
    "PublicationDate": "17/09/2019",
    "PropertyNumber": 2198231,
    "County": "CAVAN",
    "LocalAuthority": "CAVAN COUNTY COUNCIL",
    "Valuation": 15090,
    "Category": "OFFICE",
    "Uses": "OFFICE (OVER THE SHOP), -",
    "Address1": "77(1ST & 2ND FL) MAIN STREET",
    "Address2": "CAVAN",
    "Address3": "CO. CAVAN",
    "Address4": "",
    "Address5": "",
    "CarPark": 0,
    "Xitm": 641927.73,
    "Yitm": 804638.3,
    "ValuationReport": [
      {
        "Level": "1",
        "FloorUse": "RESTAURANT",
        "Area": 112.25,
        "NavPerM2": 80,
        "Nav": 8980
      },
      {
        "Level": "2",
        "FloorUse": "RESTAURANT",
        "Area": 98.57,
        "NavPerM2": 62,
        "Nav": 6111.34
      }
    ]
  },
  {
    "PublicationDate": "17/09/2019",
    "PropertyNumber": 1558322,
    "County": "CAVAN",
    "LocalAuthority": "CAVAN COUNTY COUNCIL",
    "Valuation": 10200,
    "Category": "OFFICE",
    "Uses": "OFFICE (OWN DOOR), -",
    "Address1": "23E MAIN STREET",
    "Address2": "CAVAN",
    "Address3": "CO. CAVAN",
    "Address4": "",
    "Address5": "",
    "CarPark": 0,
    "Xitm": 641941.19,
    "Yitm": 804875.56,
    "ValuationReport": [
      {
        "Level": "0",
        "FloorUse": "OFFICE(S)",
        "Area": 127.55,
        "NavPerM2": 80,
        "Nav": 10204
      }
    ]
  },
  {
    "PublicationDate": "17/09/2019",
    "PropertyNumber": 2116802,
    "County": "CAVAN",
    "LocalAuthority": "CAVAN COUNTY COUNCIL",
    "Valuation": 15140,
    "Category": "OFFICE",
    "Uses": "OFFICE (OWN DOOR), HERITAGE / INTERPRETATIVE CENTRE",
    "Address1": "LOCAL NO/MAP REF: 7AB",
    "Address2": "MULLAGH",
    "Address3": "BAILIEBORO CO. CAVAN",
    "Address4": "",
    "Address5": "",
    "CarPark": 0,
    "Xitm": 668656.19,
    "Yitm": 785281.05,
    "ValuationReport": [
      {
        "Level": "0",
        "FloorUse": "OFFICE(S)",
        "Area": 252.49,
        "NavPerM2": 60,
        "Nav": 15149.4
property_id: unit[property.id]
      }
    ]
  }
]

The difficulty I am having is with the ValuationReport attribute which is an array of varying size for each element in the JSON array of properties. So some properties will have a ValuationReport array containing only one object whereas other properties will contain a ValuationReport object with several objects. I would like to save each of these nested objects from each ValuationReport array to a separate unit in my database (unit model).

I have used the JSONPath Gem to try to access the required elements but there is something I am doing wrong.

When I run '$ rails db:seed', it is now running until a point and I am receiving the following errors:

Creating property 2198231
Creating unit.
rails aborted!
TypeError: no implicit conversion of String into Integer
/db/seeds.rb:62:in `[]'
/db/seeds.rb:62:in `block (2 levels) in properties'
/db/seeds.rb:58:in `each'
/db/seeds.rb:58:in `block in properties'
/db/seeds.rb:36:in `each'
/db/seeds.rb:36:in `properties'
/db/seeds.rb:207:in `<top (required)>'
bin/rails:4:in `require'
bin/rails:4:in `<main>'
Tasks: TOP => db:seed
(See full trace by running task with --trace)

Here are the relevant Gems from my Gemfile:

# REST Client for APIs
gem 'rest-client', '~> 2.1'

# JSONPath - for acessing values in nested JSON objects.
gem 'jsonpath', '~> 0.5.8'

My database schema is below:

ActiveRecord::Schema.define(version: 2020_11_07_130018) do

  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  create_table "properties", force: :cascade do |t|
    t.date "publication_date"
    t.string "property_number"
    t.string "county"
    t.string "local_authority"
    t.decimal "valuation"
    t.string "category"
    t.string "uses"
    t.string "address_1"
    t.string "address_2"
    t.string "address_3"
    t.string "address_4"
    t.string "address_5"
    t.decimal "car_park"
    t.decimal "xitm"
    t.decimal "yitm"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "units", force: :cascade do |t|
    t.string "level"
    t.string "floor_use"
    t.decimal "area"
    t.decimal "nav_per_m2"
    t.decimal "nav"
    t.bigint "property_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["property_id"], name: "index_units_on_property_id"
  end

  add_foreign_key "units", "properties"
end

My seeds.rb file is below. I have tried to use an index (i) to ensure that the loop for the ValuationReport array of objects is looping once for each property and creating all of the units that exist within each property.

require 'rest-client'
require 'json'
require 'jsonpath'

# Define Method - API Request
def properties

    response = RestClient.get('https://api.valoff.ie/api/Property/GetProperties?Fields=*&LocalAuthority=CAVAN%20COUNTY%20COUNCIL&CategorySelected=OFFICE&Format=csv&Download=false')
    json = JSON.parse(response)

    json.each do |property|
        puts "Creating property #{property['PropertyNumber']}"

        Property.create!(

            publication_date: property['PublicationDate'],
            property_number: property['PropertyNumber'],
            county: property['County'],
            local_authority: property['LocalAuthority'],
            valuation: property['Valuation'],
            category: property['Category'],
            uses: property['Uses'],
            address_1: property['Address1'],
            address_2: property['Address2'],
            address_3: property['Address3'],
            address_4: property['Address4'],
            address_5: property['Address5'],
            car_park: property['CarPark'],
            xitm: property['Xitm'],
            yitm: property['Yitm'],
            units: 
                JsonPath.new('$.ValuationReport').on(property).each do |unit|
                    puts "Creating unit."
                    Unit.create!(
                        level: unit['$.Level'],
                        floor_use: unit['$.FloorUse'],
                        area: unit['$.Area'],
                        nav_per_m2: unit['$.NavPerM2'],
                        nav: unit['$.Nav'],
                        property_id: unit['property.id']
                    )
                end
        )
    end
end

# Call Method
properties

Appreciate your time and help!


Solution

  • In this scenario you do not need JsonPath. I updated your script:

    • removed JsonPath in favour of using property['ValuationReport']
    • removed $. from json keys
    • first you should create a Property and then use its ID in Units (alternatively you can setup accepts_nested_attributes in models)
    require 'rest-client'
    require 'json'
    require 'jsonpath'
    
    # Define Method - API Request
    def properties
    
        response = RestClient.get('https://api.valoff.ie/api/Property/GetProperties?Fields=*&LocalAuthority=CAVAN%20COUNTY%20COUNCIL&CategorySelected=OFFICE&Format=csv&Download=false')
        json = JSON.parse(response)
    
        json.each do |property|
            puts "Creating property #{property['PropertyNumber']}"
    
            property_model = Property.create!(
    
                publication_date: property['PublicationDate'],
                property_number: property['PropertyNumber'],
                county: property['County'],
                local_authority: property['LocalAuthority'],
                valuation: property['Valuation'],
                category: property['Category'],
                uses: property['Uses'],
                address_1: property['Address1'],
                address_2: property['Address2'],
                address_3: property['Address3'],
                address_4: property['Address4'],
                address_5: property['Address5'],
                car_park: property['CarPark'],
                xitm: property['Xitm'],
                yitm: property['Yitm']
            )
    
    
            property['ValuationReport'].each do |unit|
              puts "Creating unit."
              property_model.units.create!(
                level: unit['Level'],
                floor_use: unit['FloorUse'],
                area: unit['Area'],
                nav_per_m2: unit['NavPerM2'],
                nav: unit['Nav']
              )
            end
        end
    end
    
    # Call Method
    properties