Proper consumption of difficult nested JSON objects in Scala using spray-json and scalar.http

239 views Asked by At

My problem is basically to deserialize a complex json string to an object.

This is the case classes with implicits:

import spray.json.{RootJsonFormat, _}

case class OpenCageDataDocumentation(value: String) extends AnyVal
case class OpenCageDataLicence(name: String, url: String)
case class OpenCageDataRate(limit: Double, remaining: Double, reset: Double)
case class OpenCageDataStatus(code: Int, message: String)
case class OpenCageDataStayInformed(blog: String, twitter: String)
case class OpenCageDataTimestamp(created_http: String, created_unix: Long)
case class OpenCageResultGeometry(lat: Double, lng: Double)
case class OpenCageResultAnnotation(DMS: Map[String, String],
                                    FIPS: Map[String, String],
                                    MGRS: String,
                                    Maidenhead: String,
                                    Mercator: Map[String, Double],
                                    OSM: Map[String, String],
                                    UN_M49: Map[String, Either[Map[String, String], List[String]]],
                                    callingcode: Long,
                                    currency: Map[String, Either[String, Either[Double, List[String]]]],
                                    flag: String,
                                    geohash: String,
                                    qibla: Double,
                                    roadinfo: Map[String, String],
                                    sun: Map[String, Map[String, Double]],
                                    timezone: Map[String, Either[String, Double]],
                                    what3words: Map[String, String])
case class OpenCageDataResult(geometry: Option[OpenCageResultGeometry],
                              annotations: Map[String, OpenCageResultAnnotation],
                              confidence: Long,
                              formatted: String,
                              components: Map[String, String],
                              bounds: Map[String, OpenCageResultGeometry])

case class OpenCageDataResponse(documentation: String,
                                licenses: List[OpenCageDataLicence],
                                rate: OpenCageDataRate,
                                results: List[OpenCageDataResult],
                                status: OpenCageDataStatus,
                                stay_informed: OpenCageDataStayInformed,
                                thanks: String,
                                timestamp: OpenCageDataTimestamp,
                                total_results: Int)

trait OpenCageJsonFormatResponse extends DefaultJsonProtocol {
  implicit val jsonFormatResponse: RootJsonFormat[OpenCageDataResponse] = jsonFormat9(OpenCageDataResponse)
  implicit val jsonFormatLicence: RootJsonFormat[OpenCageDataLicence] = jsonFormat2(OpenCageDataLicence)
  implicit val jsonFormatRate: RootJsonFormat[OpenCageDataRate] = jsonFormat3(OpenCageDataRate)
  implicit val jsonFormatStatus: RootJsonFormat[OpenCageDataStatus] = jsonFormat2(OpenCageDataStatus)
  implicit val jsonFormatStayInformed: RootJsonFormat[OpenCageDataStayInformed] = jsonFormat2(OpenCageDataStayInformed)
  implicit val jsonFormatTimestamp: RootJsonFormat[OpenCageDataTimestamp] = jsonFormat2(OpenCageDataTimestamp)
  implicit val jsonFormatGeometry: RootJsonFormat[OpenCageResultGeometry] = jsonFormat2(OpenCageResultGeometry)
  implicit val jsonFormatResult: RootJsonFormat[OpenCageDataResult] = jsonFormat6(OpenCageDataResult)
  implicit val jsonFormatResultAnnotation: RootJsonFormat[OpenCageResultAnnotation] = jsonFormat16(OpenCageResultAnnotation)
}

This is the main object:

import scalaj.http.{Http, HttpResponse}

import spray.json._


object testapi2 extends OpenCageJsonFormatResponse {

  private final val BASE_OPEN_CAGE_DATA_URI = s"https://api.opencagedata.com/geocode/v1/json?"
  private final val OPEN_CAGE_DATA_API_KEY = sys.env("OPEN_CAGE_DATA_API_KEY")

  def main(args: Array[String]): Unit = {
    val response: HttpResponse[String] = Http(BASE_OPEN_CAGE_DATA_URI)
      .param("q", "500 W 120 S")
      .param("key", OPEN_CAGE_DATA_API_KEY)
      .execute()
    val json = response.body.parseJson;
    println(json)
    val parsedBody = json.convertTo[OpenCageDataResponse]
    println(parsedBody)
  }
}

It breaks on line val parsedBody = json.convertTo[OpenCageDataResponse] with the following trace:

Exception in thread "main" java.lang.NullPointerException
    at spray.json.JsValue.convertTo(JsValue.scala:33)
    at spray.json.CollectionFormats$$anon$1.$anonfun$read$1(CollectionFormats.scala:30)
    at scala.collection.Iterator$$anon$10.next(Iterator.scala:459)
    at scala.collection.Iterator.foreach(Iterator.scala:941)
    at scala.collection.Iterator.foreach$(Iterator.scala:941)
    at scala.collection.AbstractIterator.foreach(Iterator.scala:1429)
    at scala.collection.generic.Growable.$plus$plus$eq(Growable.scala:62)
    at scala.collection.generic.Growable.$plus$plus$eq$(Growable.scala:53)
    at scala.collection.mutable.ListBuffer.$plus$plus$eq(ListBuffer.scala:184)
    at scala.collection.mutable.ListBuffer.$plus$plus$eq(ListBuffer.scala:47)
    at scala.collection.TraversableOnce.to(TraversableOnce.scala:315)
    at scala.collection.TraversableOnce.to$(TraversableOnce.scala:313)
    at scala.collection.AbstractIterator.to(Iterator.scala:1429)
    at scala.collection.TraversableOnce.toList(TraversableOnce.scala:299)
    at scala.collection.TraversableOnce.toList$(TraversableOnce.scala:299)
    at scala.collection.AbstractIterator.toList(Iterator.scala:1429)
    at spray.json.CollectionFormats$$anon$1.read(CollectionFormats.scala:30)
    at spray.json.CollectionFormats$$anon$1.read(CollectionFormats.scala:27)
    at spray.json.ProductFormats.fromField(ProductFormats.scala:58)
    at spray.json.ProductFormats.fromField$(ProductFormats.scala:51)
    at test.scala$.fromField(scala.scala:20)
    at spray.json.ProductFormatsInstances$$anon$9.read(ProductFormatsInstances.scala:262)
    at spray.json.ProductFormatsInstances$$anon$9.read(ProductFormatsInstances.scala:245)
    at spray.json.JsValue.convertTo(JsValue.scala:33)
    at test.scala$.main(scala.scala:36)
    at test.scala.main(scala.scala)

And this is the actual json object coming back as a response

{
    "documentation": "https://opencagedata.com/api",
    "licenses": [
        {
            "name": "see attribution guide",
            "url": "https://opencagedata.com/credits"
        }
    ],
    "rate": {
        "limit": 2500,
        "remaining": 2493,
        "reset": 1628899200
    },
    "results": [
        {
            "annotations": {
                "DMS": {
                    "lat": "40° 45' 59.93748'' N",
                    "lng": "111° 54' 20.11104'' W"
                },
                "FIPS": {
                    "county": "49035",
                    "state": "49"
                },
                "MGRS": "12TVL2357013247",
                "Maidenhead": "DN40bs13hx",
                "Mercator": {
                    "x": -12457272.898,
                    "y": 4950075.893
                },
                "OSM": {
                    "edit_url": "https://www.openstreetmap.org/edit?way=134275081#map=17/40.76665/-111.90559",
                    "note_url": "https://www.openstreetmap.org/note/new#map=17/40.76665/-111.90559&layers=N",
                    "url": "https://www.openstreetmap.org/?mlat=40.76665&mlon=-111.90559#map=17/40.76665/-111.90559"
                },
                "UN_M49": {
                    "regions": {
                        "AMERICAS": "019",
                        "NORTHERN_AMERICA": "021",
                        "US": "840",
                        "WORLD": "001"
                    },
                    "statistical_groupings": [
                        "MEDC"
                    ]
                },
                "callingcode": 1,
                "currency": {
                    "alternate_symbols": [
                        "US$"
                    ],
                    "decimal_mark": ".",
                    "disambiguate_symbol": "US$",
                    "html_entity": "$",
                    "iso_code": "USD",
                    "iso_numeric": "840",
                    "name": "United States Dollar",
                    "smallest_denomination": 1,
                    "subunit": "Cent",
                    "subunit_to_unit": 100,
                    "symbol": "$",
                    "symbol_first": 1,
                    "thousands_separator": ","
                },
                "flag": "",
                "geohash": "9x0rvt2ffy4zsqbc414g",
                "qibla": 28.5,
                "roadinfo": {
                    "drive_on": "right",
                    "road": "500 West",
                    "speed_in": "mph"
                },
                "sun": {
                    "rise": {
                        "apparent": 1628858280,
                        "astronomical": 1628852040,
                        "civil": 1628856480,
                        "nautical": 1628854320
                    },
                    "set": {
                        "apparent": 1628821560,
                        "astronomical": 1628827800,
                        "civil": 1628823360,
                        "nautical": 1628825460
                    }
                },
                "timezone": {
                    "name": "America/Denver",
                    "now_in_dst": 1,
                    "offset_sec": -21600,
                    "offset_string": "-0600",
                    "short_name": "MDT"
                },
                "what3words": {
                    "words": "means.detect.taxi"
                }
            },
            "bounds": {
                "northeast": {
                    "lat": 40.7666993,
                    "lng": -111.9055364
                },
                "southwest": {
                    "lat": 40.7665993,
                    "lng": -111.9056364
                }
            },
            "components": {
                "ISO_3166-1_alpha-2": "US",
                "ISO_3166-1_alpha-3": "USA",
                "_category": "commerce",
                "_type": "retail",
                "city": "Salt Lake City",
                "continent": "North America",
                "country": "United States",
                "country_code": "us",
                "county": "Salt Lake County",
                "house_number": "120",
                "postcode": "84101",
                "retail": "North",
                "road": "500 West",
                "state": "Utah",
                "state_code": "UT"
            },
            "confidence": 9,
            "formatted": "North, 120 500 West, Salt Lake City, UT 84101, United States of America",
            "geometry": {
                "lat": 40.7666493,
                "lng": -111.9055864
            }
        },
        {
            "annotations": {
                "DMS": {
                    "lat": "40° 53' 18.11256'' N",
                    "lng": "111° 53' 32.41824'' W"
                },
                "FIPS": {
                    "county": "49011",
                    "state": "49"
                },
                "MGRS": "12TVL2482626747",
                "Maidenhead": "DN40bv23we",
                "Mercator": {
                    "x": -12455798.136,
                    "y": 4967913.265
                },
                "OSM": {
                    "edit_url": "https://www.openstreetmap.org/edit?way=684890599#map=17/40.88836/-111.89234",
                    "note_url": "https://www.openstreetmap.org/note/new#map=17/40.88836/-111.89234&layers=N",
                    "url": "https://www.openstreetmap.org/?mlat=40.88836&mlon=-111.89234#map=17/40.88836/-111.89234"
                },
                "UN_M49": {
                    "regions": {
                        "AMERICAS": "019",
                        "NORTHERN_AMERICA": "021",
                        "US": "840",
                        "WORLD": "001"
                    },
                    "statistical_groupings": [
                        "MEDC"
                    ]
                },
                "callingcode": 1,
                "currency": {
                    "alternate_symbols": [
                        "US$"
                    ],
                    "decimal_mark": ".",
                    "disambiguate_symbol": "US$",
                    "html_entity": "$",
                    "iso_code": "USD",
                    "iso_numeric": "840",
                    "name": "United States Dollar",
                    "smallest_denomination": 1,
                    "subunit": "Cent",
                    "subunit_to_unit": 100,
                    "symbol": "$",
                    "symbol_first": 1,
                    "thousands_separator": ","
                },
                "flag": "",
                "geohash": "9x22tg6rzx97d57jjnu3",
                "qibla": 28.49,
                "roadinfo": {
                    "drive_on": "right",
                    "road": "500 West",
                    "speed_in": "mph"
                },
                "sun": {
                    "rise": {
                        "apparent": 1628858220,
                        "astronomical": 1628851980,
                        "civil": 1628856480,
                        "nautical": 1628854320
                    },
                    "set": {
                        "apparent": 1628821560,
                        "astronomical": 1628827800,
                        "civil": 1628823360,
                        "nautical": 1628825520
                    }
                },
                "timezone": {
                    "name": "America/Denver",
                    "now_in_dst": 1,
                    "offset_sec": -21600,
                    "offset_string": "-0600",
                    "short_name": "MDT"
                },
                "what3words": {
                    "words": "riding.grass.supply"
                }
            },
            "bounds": {
                "northeast": {
                    "lat": 40.8884146,
                    "lng": -111.8922884
                },
                "southwest": {
                    "lat": 40.8883146,
                    "lng": -111.8923884
                }
            },
            "components": {
                "ISO_3166-1_alpha-2": "US",
                "ISO_3166-1_alpha-3": "USA",
                "_category": "building",
                "_type": "building",
                "continent": "North America",
                "country": "United States",
                "country_code": "us",
                "county": "Davis County",
                "house_number": "120",
                "postcode": "84010",
                "road": "500 West",
                "state": "Utah",
                "state_code": "UT",
                "town": "Bountiful"
            },
            "confidence": 10,
            "formatted": "120 500 West, Bountiful, UT 84010, United States of America",
            "geometry": {
                "lat": 40.8883646,
                "lng": -111.8923384
            }
        },
        {
            "annotations": {
                "DMS": {
                    "lat": "15° 30' 0.00000'' S",
                    "lng": "35° 0' 0.00000'' E"
                },
                "MGRS": "36LYH1454485369",
                "Maidenhead": "KH74mm00aa",
                "Mercator": {
                    "x": 3896182.178,
                    "y": -1735479.281
                },
                "OSM": {
                    "note_url": "https://www.openstreetmap.org/note/new#map=17/-15.50000/35.00000&layers=N",
                    "url": "https://www.openstreetmap.org/?mlat=-15.50000&mlon=35.00000#map=17/-15.50000/35.00000"
                },
                "UN_M49": {
                    "regions": {
                        "AFRICA": "002",
                        "EASTERN_AFRICA": "014",
                        "MW": "454",
                        "SUB-SAHARAN_AFRICA": "202",
                        "WORLD": "001"
                    },
                    "statistical_groupings": [
                        "LDC",
                        "LEDC",
                        "LLDC"
                    ]
                },
                "callingcode": 265,
                "currency": {
                    "alternate_symbols": [],
                    "decimal_mark": ".",
                    "format": "%n %u",
                    "html_entity": "",
                    "iso_code": "MWK",
                    "iso_numeric": "454",
                    "name": "Malawian Kwacha",
                    "smallest_denomination": 1,
                    "subunit": "Tambala",
                    "subunit_to_unit": 100,
                    "symbol": "MK",
                    "symbol_first": 0,
                    "thousands_separator": ","
                },
                "flag": "",
                "geohash": "kv0zu6q1zned3z8us7yj",
                "qibla": 7.44,
                "roadinfo": {
                    "drive_on": "left",
                    "speed_in": "km/h"
                },
                "sun": {
                    "rise": {
                        "apparent": 1628827140,
                        "astronomical": 1628822760,
                        "civil": 1628825820,
                        "nautical": 1628824260
                    },
                    "set": {
                        "apparent": 1628868660,
                        "astronomical": 1628873040,
                        "civil": 1628869980,
                        "nautical": 1628871540
                    }
                },
                "timezone": {
                    "name": "Africa/Maputo",
                    "now_in_dst": 0,
                    "offset_sec": 7200,
                    "offset_string": "+0200",
                    "short_name": "CAT"
                },
                "what3words": {
                    "words": "inventing.amiability.pastures"
                }
            },
            "components": {
                "ISO_3166-1_alpha-2": "MW",
                "ISO_3166-1_alpha-3": "MWI",
                "_category": "place",
                "_type": "state",
                "continent": "Africa",
                "country": "Malawi",
                "country_code": "mw",
                "state": "Southern Region"
            },
            "confidence": 9,
            "formatted": "Southern Region, Malawi",
            "geometry": {
                "lat": -15.5,
                "lng": 35.0
            }
        }
    ],
    "status": {
        "code": 200,
        "message": "OK"
    },
    "stay_informed": {
        "blog": "https://blog.opencagedata.com",
        "twitter": "https://twitter.com/OpenCage"
    },
    "thanks": "For using an OpenCage API",
    "timestamp": {
        "created_http": "Fri, 13 Aug 2021 18:18:10 GMT",
        "created_unix": 1628878690
    },
    "total_results": 3
}

I don't really think that this is a good way of consuming objects in terms of shortness of a code, but I really didn't find anything better, and so appreciate for sharing a better approach to use further.

0

There are 0 answers