Search code examples
jsoncrystal-lang

How To Parse JSON From Doubly Nested Response


If I am getting a JSON response for a cars endpoint:

{
  data: {
    cars: [
      { make: "Tesla", model: "S" }
    ]
  }
}

And I have a class Car:

class Car
  JSON.mapping(
    make: String,
    model: String
  )
end

How can I parse the response so that the root goes down two levels? I know that I can do Car.from_json(json_string, "data"), but cannot find a way to specify another root key.

If there is no way to do this, what would be the simplest way without creating another class just for the cars level?


Solution

  • There's several approaches you can take here.

    One simple, albeit least efficient, is to just parse twice:

    cars = Array(Car).from_json(JSON.parse(json)["data"]["cars"].to_json)
    

    https://carc.in/#/r/6vnk

    Then as you mentioned, you can just create mappings for the outer classes. There's no real faster way compared to what you have:

    class Root
      JSON.mapping(data: Data)
    end
    
    class Data
      JSON.mapping(cars: Array(Car))
    end
    
    class Car
      JSON.mapping(
        make: String,
        model: String
      )
    end
    
    cars = Root.from_json(json).data.cars
    

    https://carc.in/#/r/6vnm

    Or if you prefer using the JSON::Serializable API:

    class Root
      include JSON::Serializable
      property data : Data
    end
    
    class Data
      include JSON::Serializable
      property cars : Array(Car)
    end
    
    class Car
      include JSON::Serializable
      property  make : String
      property  model : String
    end
    
    cars = Root.from_json(json).data.cars
    

    https://carc.in/#/r/6vno

    Finally, a last approach is to drive the JSON::PullParser API directly:

    parser = JSON::PullParser.new(json)
    cars = nil
    parser.on_key("data") do
      parser.on_key("cars") do
        cars = Array(Car).new(parser)
      end
    end
    

    https://carc.in/#/r/6vni