pathikrit / dijon

A Dynamically Typed Scala Json Library

GitHub

Build Status Coverage Status

dijon - Dynamic Json in Scala

  • Boiler-free json wrangling using Scala Dynamic Types
  • Support of RFC8259 using a codec based on jsoniter-scala-core
  • Less than 200 lines of code
  • Well tested
  • Why yet another Scala json library? Well, code speaks more than thousand words:
val (name, age) = ("Tigri", 7)
val cat = json"""
  {
    "name": "$name",
    "age": $age,
    "hobbies": ["eating", "purring"],
    "is cat": true
  }
"""
assert(cat.name == name)                         // dynamic type
assert(cat.age == age)
val Some(catAge: Int) = cat.age.asInt
assert(catAge == age)
assert(cat.age.asBoolean == None)

val catMap = cat.toMap                           // view as a hashmap
assert(catMap.keySet == Set("name", "age", "hobbies", "is cat"))

assert(cat.hobbies(1) == "purring") // array access
assert(cat.hobbies(100) == None)    // missing element
assert(cat.`is cat` == true)        // keys with spaces/symbols/scala-keywords need to be escaped with ticks
assert(cat.email == None)           // missing key

val vet = `{}`                      // create empty json object
vet.name = "Dr. Kitty Specialist"   // set attributes in json object
vet.phones = `[]`                   // create empty json array
val phone = "(650) 493-4233"
vet.phones(2) = phone               // set the 3rd item in array to this phone
assert(vet.phones == mutable.Seq(null, null, phone))  // first 2 entries null

vet.address = `{}`
vet.address.name = "Animal Hospital"
vet.address.city = "Palo Alto"
vet.address.zip = 94306
assert(vet.address == mutable.Map[String, SomeJson]("name" -> "Animal Hospital", "city" -> "Palo Alto", "zip" -> 94306))

cat.vet = vet                            // set the cat.vet to be the vet json object we created above
assert(cat.vet.phones(2) == phone)
assert(cat.vet.address.zip == 94306)     // json deep access

println(cat) // {"name" : "Tigri", "hobbies" : ["eating", "purring"], "vet" : {"address" : {"city" : "Palo Alto", "zip" : 94306, "name" : "Animal Hospital"}, "name" : "Dr. Kitty Specialist", "phones" : [null, null, "(650) 493-4233"]}, "is cat" : true, "age" : 7.0}
assert(cat == parse(cat.toString))   // round-trip test

var basicCat = cat -- "vet"                                  // remove 1 key
basicCat = basicCat -- ("hobbies", "is cat", "paws")         // remove multiple keys ("paws" is not in cat)
assert(basicCat == json"""{ "name": "Tigri", "age": 7}""")   // after dropping some keys above
  • Simple deep-merging:
val scala = json"""
{
  "name": "scala",
  "version": "2.13.0",
  "features": {
    "functional": true,
    "awesome": true
  }
}
"""

val java = json"""
{
  "name": "java",
  "features": {
    "functional": [0, 0],
    "terrible": true
  },
  "bugs": 213
}
"""

assert((scala ++ java) == json"""{"name": "java", "version": "2.13.0", "features": { "functional": [0, 0], "terrible": true, "awesome": true}, "bugs": 213}""")
assert((java ++ scala) == json"""{"name": "scala", "version": "2.13.0", "features": { "functional": true, "terrible": true, "awesome": true}, "bugs": 213}""")
val json = `{}`
json.aString = "hi"                        // compiles
json.aBoolean = true                       // compiles
json.anInt = 23                            // compiles
//json.somethingElse = Option("hi")       // does not compile
val Some(i: Int) = json.anInt.asInt
assert(i == 23)
assert(json.aBoolean.asInt == None)

See the spec for more examples.

Also, for the dijon.codec an additional functionality is available when using jsoniter-scala-core, like:

  • parsing/serialization from/to byte arrays, byte buffers, and input/output streams
  • parsing of streamed JSON values (concatenated or delimited by whitespace characters) and JSON arrays from input streams using callbacks without the need of holding a whole input in the memory
  • use a custom configuration for parsing and serializing

See jsoniter-scala-core spec for more details and code samples.

Usage

  1. Add the following to your build.sbt:
libraryDependency += "com.github.pathikrit" %% "dijon" % "0.3.0"
  1. Turn on support of dynamic types by adding import clause:
import scala.language.dynamics._

or by setting the scala compiler option:

scalacOptions += "-language:dynamics"
  1. Add import of the package object of dijon for the main functionality:
import com.github.pathikrit.dijon._
  1. Optionally, add import of package object of jsoniter-scala-core for an extended functionality:
import com.github.plokhotnyuk.jsoniter_scala.core._

TODO

  • BigInt support
  • Circular references checker
  • YAML interpolator
  • Macro for type inference to induce compile-time errors where possible
  • JSON string interpolator fills in braces, quotes and commas etc
  • Remove warnings