Skip to content

jvican/dijon

Folders and files

NameName
Last commit message
Last commit date
Nov 9, 2022
Nov 28, 2020
Mar 23, 2022
Jun 29, 2024
Nov 28, 2020
Nov 28, 2020
Nov 17, 2023
Nov 2, 2021
Apr 17, 2024

Repository files navigation

Continuous Integration

dijon - Dynamic JSON in Scala

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.toMap.keysIterator.toSeq == Seq("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(None, None, phone))  // first 2 entries None

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","age":7,"hobbies":["eating","purring"],"is cat":true,"vet":{"name":"Dr. Kitty Specialist","phones":[null,null,"(650) 493-4233"],"address":{"name":"Animal Hospital","city":"Palo Alto","zip":94306}}}

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.2",
  "features": {
    "functional": true,
    "awesome": true
  }
}
"""

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

val scalaCopy = scala.deepCopy
val javaCopy = java.deepCopy

assert((scala ++ java) == json"""{"name":"java","version":"2.13.2","features":{"functional":[0,0],"terrible":true,"awesome":true},"bugs":213}""")
assert((java ++ scala) == json"""{"name":"scala","version":"2.13.2","features":{"functional": true,"terrible":true,"awesome":true},"bugs":213}""")

assert(scala == scalaCopy)       // original json objects stay untouched after merging
assert(java == javaCopy)
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)
  • obj() and arr() constructor functions for building up complex JSON values with less overhead:
val rick = obj(
  "name" -> name,
  "age" -> age,
  "class" -> "human",
  "weight" -> 175.1,
  "is online" -> true,
  "contact" -> obj(
    "emails" -> arr(email1, email2),
    "phone" -> obj(
      "home" -> "817-xxx-xxx",
      "work" -> "650-xxx-xxx"
    )
  ),
  "hobbies" -> arr(
    "eating",
    obj(
      "games" -> obj(
        "chess" -> true,
        "football" -> false
      )
    ),
    arr("coding", arr("python", "scala")),
    None
  ),
  "toMap" -> arr(23, 345, true),
  "apply" -> 42
)

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 += "me.vican.jorge" %% "dijon" % "0.6.0" // Use %%% instead of %% for Scala.js
  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 dijon._
  1. Optionally, add import of package object of jsoniter-scala-core for extended json 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