I received some very good feedback on my last post: Scala has some popular libraries that should be avoided. Not only that, I learned a few libraries I considered before required more investigation. Today I want to look at Circe. After working in Scala a bit, I understand the problems it wants to solve and why the project makes the choices it does.
Cursors
Think about how you use JSON for a minute. Programs retrieve or assign to a single field at a time. Parsing the entire document when the program only needs one field wastes processing time and heap memory.
car["age"] = 23
console.log(car["age"])
When I wrote more NodeJS I would not bat an eye at parsing one document entirely, but when working with many documents or a large document I would use JSONStream to selectively parse items out of the document. This provides two benefits: it does not require storing the entire document into memory and only the pertinent fields parse.
Circe provides "cursors", an API to traverse raw JSON and project the values therein. Think about cursors the way you think about the blinking |
cursor when writing an email or text. It keeps track of your place in the document, moves around the document to track the new current location, and even selects interesting text for modification.
Example
I'm assuming some sbt
knowledge. Here's the build.sbt
I used to run this project.
name := "basic_json"
version := "0.1"
scalaVersion := "2.12.2"
lazy val circeVersion = "0.12.2"
libraryDependencies ++= Seq(
"io.circe" %% "circe-parser" % circeVersion,
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion
)
This example iterates through a JSON document via a cursor to select and present the car's age the document describes.
import io.circe.parser._
object Main extends App {
val rawJson: String =
"""
|{
| "hello": "world",
| "car": {
| "type": "honda",
| "age": 23,
| "color": "blue",
| "used": true,
| "owner": {
| "name": "darwin",
| "age": 43
| }
| }
|}
|""".stripMargin
val parsed = parse(rawJson) match {
case Left(s) => s
case Right(r) => r.hcursor.downField("car").get[Int]("age") match {
case Right(age) => age
case Left(l) => l
}
}
println(parsed)
}
A lot going on, but focusing on the critical area simplifies things:
val parsed = parse(rawJson) match {
case Left(s) => s
case Right(r) => r.hcursor.downField("car").get[Int]("age") match {
case Right(age) => age
case Left(l) => l
}
}
Circe's parse
from import io.circe.parser._
ingests the raw JSON document and returns an Either
. Left
contains useful information about failures, while Right
contains success values. I always return the unwrapped Left
value since I just want to log results, but ideally Lefts
would branch code into an error handling branch.
Finally, the important piece: actually retrieving the data.
r.hcursor.downField("car").get[Int]("age")
r
represents the Right
result of parse
which contains an hcursor
into the JSON document. HCursors
maintain a cursor's history so relevant history may surface during an error indicating why the cursor operation failed.
downField("x")
moves the cursor's position into the JSON
object specified by "x"
. Very similar to how JavaScript accesses object via obj["x"]
. get[Type]("field")
performs a downField
to the "field"
key, parses the JSON
object at that point, and then casts it to the type specified. In my case I cast the "age"
field to an Int
. Casting tells Scala to take an object it thought was text and convert it into a Scala object like an Int
.
Conclusion
Scala definitely requires more thought when working with JSON. While Circe's techniques seem radically different from Javascript at first glance; these languages really have a lot in common. As an exercise try triggering different Left
branches to see the messages they produce.
Top comments (0)