Peninsula is a Scala lib providing a collection of useful tools working with json AST and facilitating json transformations without ever converting your jsons into domain objects.
Its main goal is to eventually make building data centric coast-to-coast applications an easier and a more intuitive process.
Technically Peninsula is an abstraction layer on top of Json4s.
Disclaimer: even if it's used on production at Wix - this is an early stage lib - with plenty of space for optimization and improvement. Contributions and comments are very welcome!
Add the following dependency to your pom if you use maven
<dependency>
<groupId>com.wix</groupId>
<artifactId>peninsula</artifactId>
<version>0.1.5</version>
</dependency>
For SBT users
val peninsula = "com.wix" % "peninsula" % "0.1.5"
libraryDependencies += peninsula
All examples below can also be found in the following test: ExampleTest.scala
Easily extract top level and nested values from json.
import com.wix.peninsula.Json
case class Person(id: Long, name: String)
case class Item(name: String, sale: Boolean)
val json = Json.parse(
"""
{
"id": 1,
"name": "John",
"location": {"city": "Vilnius", "country": "LT"},
"customer": { "id": 1, "name": "John"},
"items": [{"name": "tomatoes", "sale": true}, {"name": "snickers", "sale": false}]
}
""")
json.extractString("location.city")
result: "Vilnius"
json.extractStringOpt("location.city")
result: Some("Vilnius")
json.extractStringOpt("location.postCode")
result: None
json.extract[Person]("customer")
result: Person(id = 1, name = "John")
json.extract[Seq[Boolean]]("items.sale")
result: Seq(true, false)
json.extract[Seq[Item]]("items")
result: Seq(Item(name = "tomatoes", sale = true), Item(name = "snickers", sale = false))
json.extract[Item]("items[1]")
result: Item(name = "snickers", sale = false)
You can also subselect a json
by path and extract values from it
val location: Json = json("location")
location.extractString("city")
result: "Vilnius"
location.extractString("country")
result: "LT"
val items: Json = json("items")
items.extractString("[0].name")
result: "tomatoes"
items.extractString("[1].name")
result: "snickers"
items.extract[Seq[String]]("name")
result: Seq("tomatoes", "snickers")
json("location.city").exists
result: true
json("location.postCode").exists
result: false
json("items[1].name").exists
result: true
json("items[1].name").contains("snickers")
result: true
json("id").isNull
result: false
json("mobile").isNull
result: true
Here you can find comprehensive list of extraction methods.
Build a transformation configuration to describe the rules that later can be used for transforming one json into another. TransformationConfig represents a collection of copy configurations that each copy one bit from the original json into the resulting json in a specific way.
In the example below copyFields("id", "slug")
copies 2 fields id and slug as they are without making any changes to the property names of values. It expects the value to be a primitive type.
copyField("name" -> "title")
copies the name field - but renames the property to title.
copy("images")
copies the images field into the resulting json without making any assumptions about the value type. i.e. images value can be an object or an array.
import com.wix.peninsula.CopyConfigFactory._
import com.wix.peninsula._
val config = TransformationConfig()
.add(copyFields("id", "slug"))
.add(copyField("name" -> "title"))
.add(copy("images"))
val json = Json.parse(
"""
{
"id":1,
"slug":"raw-metal",
"name":"Raw Metal Gym",
"images":{
"top":"//images/top.jpg",
"background":"//images/background.png"
}
}
""")
json.transform(config)
com.wix.peninsula.Json =
{
"id": 1,
"slug": "raw-metal",
"title": "Raw Metal Gym",
"images": {
"top": "//images/top.jpg",
"background": "//images/background.png"
}
}
In the below example the mergeObject
copier merges anything in the object specified onto the top level of the resulting json.
Nested properties can be accessed using dot based selectors e.g. media.pictures.headerBackground
.
Also note how json values can be validated and transformed before copying them over to the resulting json.
import com.wix.peninsula._
import com.wix.peninsula.CopyConfigFactory._
import com.wix.peninsula.JsonValidators.NonEmptyStringValidator
import org.json4s.JsonAST.JString
object HttpsAppender extends JsonMapper {
override def map(json: Json): Json = Json(json.node match {
case JString(url) => JString("https:" + url)
case x => x
})
}
val config = TransformationConfig()
.add(copyField("id"))
.add(mergeObject("texts"))
.add(copyField("images.top" -> "media.pictures.headerBackground")
.withValidators(NonEmptyStringValidator)
.withMapper(HttpsAppender))
val json = Json.parse(
"""
{
"id":1,
"slug":"raw-metal",
"name":"Raw Metal Gym",
"texts": {
"name": "Raw metal gym",
"description": "The best gym in town. Come and visit us today!"
},
"images":{
"top":"//images/top.jpg",
"background":"//images/background.png"
}
}
""")
json.transform(config)
result: com.wix.peninsula.Json =
{
"id" : 1,
"name" : "Raw metal gym",
"description" : "The best gym in town. Come and visit us today!",
"media" : {
"pictures" : {
"headerBackground" : "https://images/top.jpg"
}
}
}
Translation differs from transformation in that it keeps the original json and merges the translation on top it. In a basic case, when the translation and the original json have the same structure - no transformation config is needed.
import com.wix.peninsula._
val json = Json.parse(
"""
{
"id":1,
"slug":"raw-metal",
"name":"Raw Metal Gym",
"images":{
"top":"//images/top.jpg",
"background":"//images/background.png"
}
}
""")
val config = Json.parse(
"""
{
"name":"Metalinis Gymas",
"images":{
"background":"//images/translated-background.png"
}
}
""")
json.translate(config)
result: com.wix.peninsula.Json =
{
"id": 1,
"slug": "raw-metal",
"name":"Metalinis Gymas",
"images": {
"top": "//images/top.jpg",
"background":"//images/translated-background.png"
}
}
import com.wix.peninsula._
import com.wix.peninsula.CopyConfigFactory._
val json = Json.parse(
"""
{
"id":1,
"slug":"raw-metal",
"name":"Raw Metal Gym",
"images":{
"top":"//images/top.jpg",
"background":"//images/background.png"
},
"features": [
{ "id": 1, "description": "Convenient location" },
{ "id": 2, "description": "Lots of space" }
]
}
""")
val translation = Json.parse(
"""
{
"title":"Metalinis Gymas",
"media": {
"backgroundImage":"//images/translated-background.png"
},
"features": [
{ "id": 2, "description": "space translated" },
{ "id": 1, "description": "location translated" }
]
}
""")
val featureConfig = TransformationConfig().add(copyField("description"))
val config = TransformationConfig()
.add(copyField("title" -> "name"))
.add(copyField("media.backgroundImage" -> "images.background"))
.add(copyArrayOfObjects(fromTo = "features", config = featureConfig, idField = "id"))
json.translate(translation, config)
result: com.wix.peninsula.Json =
{
"id": 1,
"slug": "raw-metal",
"name":"Metalinis Gymas",
"images": {
"top": "//images/top.jpg",
"background":"//images/translated-background.png"
},
"features": [
{ "id": 1, "description": "location translated" },
{ "id": 2, "description": "space translated" }
]
}
Define which fields you want to be included into the resulting json and filter all the others out. This might get in handy for implementation of restful json endpoints.
import com.wix.peninsula.Json
val json = Json.parse("""{"id": 1, "name": "John", "office": "Wix Townhall", "role": "Engineer"}""")
json.only(Set("id", "role"))
result: com.wix.peninsula.Json =
{
"id" : 1,
"role" : "Engineer"
}
There are different methods, which allow you to extract values and structures from JSON.
Methods described below look for element in specified path and return element's value in case it exists, is not null and has appropriate type. In all other cases methods throw an exception:
JsonPathDoesntExistException
if path doesn't existJsonElementIsNullException
if element's value is nullUnexpectedJsonElementException
if element's value has incorrect type
json.extract[T](path: String): T
json.extractBoolean(path: String): Boolean
json.extractInt(path: String): Int
json.extractBigInt(path: String): BigInt
json.extractLong(path: String): Long
json.extractDouble(path: String): Double
json.extractBigDecimal(path: String): BigDecimal
json.extractString(path: String): String
There are also extraction methods, which basically have the same functionality except returning Failure instead of throwing exceptions.
json.extractTry[T](path: String): Try[T]
json.extractBooleanTry(path: String): Try[Boolean]
json.extractIntTry(path: String): Try[Int]
json.extractBigIntTry(path: String): Try[BigInt]
json.extractLongTry(path: String): Try[Long]
json.extractDoubleTry(path: String): Try[Double]
json.extractBigDecimalTry(path: String): Try[BigDecimal]
json.extractStringTry(path: String): Try[String]
The second group of extraction methods called 'extractAs*' try to either extract value if it exists and has appropriate type or losslessly convert to requested type. If mission fails 'extractAs*' methods throw exception, 'extractAs*Try' methods contrariwise return Failure.
json.extractAsBoolean(path: String): Boolean
json.extractAsInt(path: String): Int
json.extractAsBigInt(path: String): BigInt
json.extractAsLong(path: String): Long
json.extractAsDouble(path: String): Double
json.extractAsBigDecimal(path: String): BigDecimal
json.extractAsString(path: String): String
json.extractAsBooleanTry(path: String): Try[Boolean]
json.extractAsIntTry(path: String): Try[Int]
json.extractAsBigIntTry(path: String): Try[BigInt]
json.extractAsLongTry(path: String): Try[Long]
json.extractAsDoubleTry(path: String): Try[Double]
json.extractAsBigDecimalTry(path: String): Try[BigDecimal]
json.extractAsStringTry(path: String): Try[String]