Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active May 27, 2023 06:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/a3c6a53fbb6cd20a60a821af70635e99 to your computer and use it in GitHub Desktop.
Save dacr/a3c6a53fbb6cd20a60a821af70635e99 to your computer and use it in GitHub Desktop.
Json4s scala json API cookbook as unit test cases. / published by https://github.com/dacr/code-examples-manager #b2de4720-2d03-44bf-92cf-1c4f67ac1864/a2291e1b8aa90f83b7f415fc7a99d9ac39199411
// summary : Json4s scala json API cookbook as unit test cases.
// keywords : scala, scalatest, json4s, json, @testable, @fail
// publish : gist
// authors : David Crosson
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2)
// id : b2de4720-2d03-44bf-92cf-1c4f67ac1864
// created-on : 2018-07-10T08:11:54Z
// managed-by : https://github.com/dacr/code-examples-manager
// execution : scala ammonite script (http://ammonite.io/) - run as follow 'amm scriptname.sc'
// run-with : scala-cli $file
// ---------------------
//> using scala "3.3.0"
//> using dep "org.scalatest::scalatest:3.2.16"
//> using dep "org.json4s::json4s-native:4.0.6"
//> using dep "org.json4s::json4s-jackson:4.0.6"
//> using dep "org.json4s::json4s-ext:4.0.6"
//> using dep "com.fasterxml.jackson.core:jackson-databind:2.15.1"
//> using objectWrapper
// ---------------------
import org.scalatest._, flatspec._, matchers._, OptionValues._
import java.time.{OffsetDateTime, ZonedDateTime}
import com.fasterxml.jackson.databind.node.ObjectNode
case class Someone(name:String, age:Int)
case class Event(when:OffsetDateTime, who:Someone, what:String)
trait Pet {
val name:String
val birthYear:Int
}
case class Dog(name:String, birthYear:Int) extends Pet
case class Cat(name:String, birthYear:Int, lifeCount:Int) extends Pet
case class Animals(animals:List[Pet])
case class Something[T](that:T)
case class CheckedValue[NUM](
value: NUM,
isPrime: Boolean,
digitCount: Long,
nth: NUM) //(implicit numops: Integral[NUM])
case class Truc(msg:String)
case class Muche[A](data:A)
case class DocTree(name:String,size:String)
case class DocReport(tree:DocTree)
case class Doc(report:DocReport)
class JsonJson4sCookBook extends AnyFlatSpec with should.Matchers {
override def suiteName = "JsonJson4sCookBook"
"json4s" should "work with literals (jackson)" in {
implicit val formats = org.json4s.DefaultFormats
import org.json4s.jackson.JsonMethods.{parse}
import org.json4s._
parse(""" "truc" """).extract[String] shouldBe """truc"""
}
it should "work with literals (native)" ignore {
implicit val formats = org.json4s.DefaultFormats
import org.json4s._
intercept[Exception] { // :(
import org.json4s.native.JsonMethods.{parse}
parse(""" "truc" """).extract[String] shouldBe """truc"""
}
fail("not supported")
}
it should "parse json strings" in {
import org.json4s.jackson.JsonMethods.{parse}
import org.json4s._
implicit val formats = org.json4s.DefaultFormats
val json = """{"name":"John Doe", "age":42}"""
val doc = parse(json)
(doc \ "name").extract[String] should equal("John Doe")
}
it should "provide easy way to query and extract data" in {
import org.json4s._
import org.json4s.Extraction.decompose
implicit val formats = org.json4s.DefaultFormats
val people = List(
Someone(name="John Doe", age=22),
Someone(name="Sarah Connors", age=84),
Someone(name="John Connors", age=42)
)
val jvalue = decompose(people)
(jvalue \\ "name").children.map(_.extract[String])
(jvalue \\ "name").children.flatMap(_.extractOpt[String])
(jvalue \\ "age").children.map(_.extract[Int]).sum shouldBe 148
}
it should "be easy easy to move from its json AST to a user friendly data structure" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{parse}
implicit val formats = org.json4s.DefaultFormats
val json = parse(""" { "truc":[{"a":1},{"a":2},{"b":3}] }""") \ "truc"
// extract alternatives, see how it is powerful to convert complex data structure from its internal AST :)
json.extract[List[JValue]]
json.extract[List[JValue]].map(_.extract[Map[String,Int]])
json.extract[List[Map[String,Int]]] shouldBe List(Map("a" -> 1), Map("a" -> 2), Map("b" -> 3))
}
it should "pretty render json" in {
import org.json4s.jackson.JsonMethods.{parse,pretty,render}
implicit val formats = org.json4s.DefaultFormats
val jvalue = parse("""{"name":"John Doe", "age":42}""")
val result: String = pretty(render(jvalue))
result.split("\n") should have size(4)
}
it should "serialize case classes to json" in {
import org.json4s.jackson.Serialization.{read, write}
implicit val formats = org.json4s.DefaultFormats
val someone = Someone(name="John Doe", age=42)
val json: String = write(someone)
read[Someone](json) should equal(someone)
}
it should "manage java8 time" in {
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.DefaultFormats
import org.json4s.ext.JavaTimeSerializers
implicit val formats = DefaultFormats ++ JavaTimeSerializers.all // Warning : by default it doesn't manage milliseconds ! See next test, and lossless method
val when = OffsetDateTime.parse("2042-01-01T01:42:42Z")
val event = Event(when, Someone("John Doe", 42), "future birth")
val json:String = write(event)
val eventBack = read[Event](json)
eventBack should equal(event)
}
it should "extract ZonedDateTime/OffsetDatetime with milliseconds" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{parse,render,compact}
import org.json4s.DefaultFormats
import org.json4s.ext.JavaTimeSerializers
implicit val formats = DefaultFormats.lossless ++ JavaTimeSerializers.all // USE .lossless for milliseconds to ba taken into account !
val timestamp = "2042-01-01T01:42:42.042Z"
val json = s"""{"when":"$timestamp"}"""
val jvalue = parse(json)
(jvalue \ "when").extract[ZonedDateTime] // Human readable date time, do not use for equals, comparison...
(jvalue \ "when").extract[OffsetDateTime] should equal(OffsetDateTime.parse(timestamp))
compact(render(jvalue)) should equal(json)
}
it should "support ISO8601 datetime timezone with offset" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{parse,render,compact}
import org.json4s.DefaultFormats
import org.json4s.ext.JavaTimeSerializers
import java.text.SimpleDateFormat
def customDefaultFormats:DefaultFormats = new DefaultFormats {
override def dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
}
implicit val formats = customDefaultFormats ++ JavaTimeSerializers.all
val timestamp = "2042-01-01T01:42:42.042+01:00"
val json = s"""{"when":"$timestamp"}"""
val jvalue = parse(json)
(jvalue \ "when").extract[ZonedDateTime] // Human readable date time, do not use for equals, comparison...
(jvalue \ "when").extract[OffsetDateTime] should equal(OffsetDateTime.parse(timestamp))
compact(render(jvalue)) should equal(json)
}
it should "have a way to detect empty object" in {
import org.json4s.jackson.JsonMethods.{parse,render,compact,pretty}
import org.json4s._
implicit val formats = DefaultFormats
implicit val serialization = jackson.Serialization
val json = """{"emptyObject":{}, "value":4242, "notEmptyObject":{"that":42} } """
val jvalue = parse(json)
val notFound1 = (jvalue \ "somethingNotHere")
notFound1 should be(JNothing)
val notFound2 = (jvalue \\ "somethingNotHere")
notFound2 should be(JObject())
val emptyObject = (jvalue \ "emptyObject").extract[JObject]
emptyObject.values.isEmpty should be(true)
emptyObject.children.isEmpty should be(true)
emptyObject.children.size should be(0)
val notEmptyObject = (jvalue \ "notEmptyObject").extract[JObject]
notEmptyObject.values.isEmpty should be(false)
notEmptyObject.children.isEmpty should be(false)
notEmptyObject.children.size should be >(0)
val aValue = (jvalue \ "value")
aValue should not be(JNothing)
aValue should not be(JObject())
aValue.extractOpt[Int] should be(Some(4242))
}
it should "provide a high level pragmatic DSL" in {
import org.json4s.jackson.JsonMethods.{parse,render,compact,pretty}
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.JsonDSL._
import org.json4s._
import Extraction.decompose
implicit val formats = DefaultFormats
implicit val serialization = Serialization
val truc = decompose(Truc("Hello"))
val muche = decompose(Muche("world"))
val reference = ("x"-> 24) ~ ("y"->42) ~ ("z1" -> truc) ~ ("z2" -> muche)
write(reference) shouldBe """{"x":24,"y":42,"z1":{"msg":"Hello"},"z2":{"data":"world"}}"""
}
it should "be possible to work with POJO objects" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{parse,render,compact,pretty}
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.JsonDSL._
import java.awt.Point
implicit val formats:Formats = DefaultFormats + FieldSerializer[Point]()
implicit val serialization = Serialization
val reference = ("x"-> 24) ~ ("y"->42)
val point = new Point(24, 42)
val jsonString = write(point)
val json = parse(jsonString)
json should equal(reference)
// Alternative way
Extraction.decompose(point) should equal(reference)
}
it should "be possible to work with POJO objects even when the class is unknown" ignore { // TODO - shall work with genson
import org.json4s.jackson.JsonMethods.{parse,render,compact,pretty}
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.JsonDSL._
import org.json4s._
val reference = ("x"-> 24) ~ ("y"->42)
//val clazz = Class.forName("java.awt.Point")
//val cons = clazz.getConstructor(classOf[Int], classOf[Int])
//val point = cons.newInstance(new java.lang.Integer(24), new java.lang.Integer(42)).asInstanceOf[Object]
val point = new java.awt.Point(24, 42)
//val customSerializer = classOf[FieldSerializer].newInstance()
implicit val formats = DefaultFormats //+ customSerializer //FieldSerializer[java.awt.Point]()
implicit val serialization = Serialization
Extraction.decompose(point) should equal(reference)
// TODO - current status : decompose doesn't introspect dynamically allocated java types
}
it should "map a ScalaMap to a json object" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{render,compact,pretty}
val now = new java.util.Date()
val nowIso8601 = now.toInstant.toString // ~ 2018-09-25T12:59:21.315Z
val input = Map("age"->42, "name"->"John Doe", "now"->now)
implicit val formats = DefaultFormats.lossless // for milliseconds in iso8601 dates...
val jvalue = Extraction.decompose(input)
val json = pretty(render(jvalue))
info(json)
compact(render(jvalue)) shouldBe s"""{"age":42,"name":"John Doe","now":"${nowIso8601}"}"""
}
it should "be possible to extract a Map from a json object" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{render,compact,pretty}
implicit val formats = DefaultFormats.lossless // for milliseconds in iso8601 dates...
val someone = Someone("john", 42)
val jvalue = Extraction.decompose(someone)
val jmap = jvalue.extract[Map[String,Any]]
jmap shouldBe Map("name"->"john", "age"->42)
}
it should "be possible to serialize java collection types" in {
import org.json4s._
import org.json4s.jackson.JsonMethods.{render,compact,pretty}
implicit val formats = DefaultFormats.lossless
val jl = new java.util.LinkedList[String]()
jl.add("Hello1")
jl.add("Hello2")
compact(render(Extraction.decompose(jl))) shouldBe """["Hello1","Hello2"]"""
}
it should "be possible to rename field during serdes operations" in {
import org.json4s._
import org.json4s.FieldSerializer
import org.json4s.FieldSerializer._
import org.json4s.jackson.Serialization.{read, write}
val renamer = FieldSerializer[Someone](
renameTo("name", "lastName"),
renameFrom("lastName", "name")
)
implicit val format: Formats = DefaultFormats + renamer
val someone = Someone(name="Connors", age=42)
val jsontxt = """{"lastName":"Connors","age":42}"""
write(someone) shouldBe jsontxt
read[Someone](jsontxt) shouldBe someone
}
it should "be possible to rename multiple fields during serdes operations" in {
import org.json4s._
import org.json4s.FieldSerializer
import org.json4s.FieldSerializer._
import org.json4s.jackson.Serialization.{read, write}
val renamer = FieldSerializer[Someone](
renameTo("name", "lastName").orElse(renameTo("age", "ageInYear")),
renameFrom("lastName", "name").orElse(renameFrom("ageInYear", "age"))
)
implicit val format: Formats = DefaultFormats + renamer
val someone = Someone(name="Connors", age=42)
val jsontxt = """{"lastName":"Connors","ageInYear":42}"""
write(someone) shouldBe jsontxt
read[Someone](jsontxt) shouldBe someone
}
it should "be possible to rename inherited field during serdes operations" in {
import org.json4s._
import org.json4s.FieldSerializer
import org.json4s.FieldSerializer._
import org.json4s.jackson.Serialization.{read, write}
val renamer = FieldSerializer[Pet](
renameTo("birthYear", "birth"),
renameFrom("birth", "birthYear")
)
implicit val format: Formats = DefaultFormats + renamer
val pet = Cat(name="minou", birthYear=1942, lifeCount=7)
val jsontxt = """{"name":"minou","birth":1942,"lifeCount":7}"""
write(pet) shouldBe jsontxt
read[Cat](jsontxt) shouldBe pet
}
it should "be possible to add type hints" in {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
implicit val formats = Serialization.formats(
FullTypeHints(List(
classOf[Cat], classOf[Dog]
))
)
val catClass = classOf[Cat].getName
val animalsText = s"""{"animals":[{"jsonClass":"$catClass","name":"minou","birthYear":1942,"lifeCount":7}]}"""
val cat = Cat(name="minou", birthYear=1942, lifeCount=7)
val animals = read[Animals](animalsText)
animals shouldBe Animals(List(cat))
write(read[Animals](animalsText)) shouldBe animalsText
}
it should "be possible to use either camel case or snake case" in {
import org.json4s._
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.jackson.JsonMethods.{parse}
import org.json4s.DefaultFormats
import org.json4s.Extraction, Extraction.decompose
import org.json4s.JValue
implicit val formats = DefaultFormats
val json = """{"name":"minou","birth_year":1980,"life_count":7}"""
val cat = Cat("minou", 1980, 7)
decompose(cat).camelizeKeys.extractOpt[Cat].value shouldBe cat
parse(json).camelizeKeys.extractOpt[Cat].value shouldBe cat
read[JValue](json).camelizeKeys.extractOpt[Cat].value shouldBe cat
}
it should "be possible to serialize / deserialize complex types with generics" in {
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.DefaultFormats
import org.json4s.Extraction, Extraction.decompose
import org.json4s.JValue
implicit val formats = DefaultFormats
val something1 = Something("chisel")
val jvalue = decompose(something1)
write(jvalue) shouldBe """{"that":"chisel"}"""
}
it should "be possible to serialize / deserialize complex types with implicits" in {
import org.json4s._
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.DefaultFormats
import org.json4s.Extraction, Extraction.decompose
import org.json4s.JValue
implicit val formats = DefaultFormats
val something1 = CheckedValue(value=7L, isPrime=true, digitCount=1, nth=4)
val jvalue = decompose(something1)
write(jvalue) shouldBe """{"value":7,"isPrime":true,"digitCount":1,"nth":4}"""
}
it should "be possible to manipulate json trees flattening" in {
import org.json4s._
import org.json4s.jackson.Serialization.{read, write}
import org.json4s.jackson.JsonMethods.{parse,compact,render}
import org.json4s.DefaultFormats
import org.json4s.Extraction, Extraction.decompose
import org.json4s.JValue
implicit val formats = DefaultFormats
val someFile: JValue = parse(
"""
|{
| "report": {
| "trees": {
| "tree": {
| "name": "oak",
| "size": "5m"
| }
| }
| }
|}
|""".stripMargin)
val subtree = (someFile \ "report" \ "trees")
val fixedtree = someFile.merge( decompose("report"->subtree)).removeField{ case (name,value)=> name == "trees"}
fixedtree.extractOpt[Doc] shouldBe Option(Doc(DocReport(DocTree("oak", "5m"))))
compact(render(fixedtree)) shouldBe """{"report":{"tree":{"name":"oak","size":"5m"}}}"""
}
"Jackson" should "be interoperable with json4s" in {
import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods.{asJsonNode, fromJsonNode}
import com.fasterxml.jackson.databind._
import org.json4s.DefaultFormats
implicit val formats = DefaultFormats
val objectMapper = new ObjectMapper()
val jsonNode = objectMapper.readTree("""{"name":"joe"}""")
info("JsonNode is the default AST type for Jackson, as JValue for json4s")
val jvalue = fromJsonNode(jsonNode)
(jvalue \ "name").extractOpt[String] shouldBe Some("joe")
asJsonNode(jvalue) shouldBe jsonNode
}
it should "be quite easy to manipulate Json AST" in {
import org.json4s._
import org.json4s.JsonDSL._
import org.json4s.jackson.JsonMethods.{asJsonNode, fromJsonNode}
import com.fasterxml.jackson.databind._
import org.json4s.DefaultFormats
implicit val formats = DefaultFormats
val objectMapper = new ObjectMapper()
val jsonNode = objectMapper.readTree("""{"name":"joe"}""")
val updatedJson = jsonNode match {
case ob:ObjectNode =>
val updated = ob.deepCopy()
updated.put("age", 42)
val addressNode = updated.putObject("address")
addressNode.put("town", "dallas")
updated
case _ => fail()
}
info("But take care, jackson use mutation, so do use deepCopy if needed as within this example")
objectMapper.writeValueAsString(updatedJson) shouldBe """{"name":"joe","age":42,"address":{"town":"dallas"}}"""
}
}
org.scalatest.tools.Runner.main(Array("-oDF", "-s", classOf[JsonJson4sCookBook].getName))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment