Last active
May 25, 2024 10:18
-
-
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/92035b1df03fd7ae2cd9fa1856015d430c67fccf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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.4.2" | |
//> 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