// summary : ZIO learning - playing with json - zio-json cheat sheet // keywords : scala, zio, learning, json, pure-functional, @testable // publish : gist // authors : David Crosson // license : Apache License Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0.txt) // license-url : // id : 862c2592-c58c-4541-817b-eaf9da4c762e // created-on : 2021-12-30T10:57:55+01:00 // managed-by : https://github.com/dacr/code-examples-manager // run-with : scala-cli $file // --------------------- //> using scala "3.4.2" //> using dep "dev.zio::zio:2.0.21" //> using dep "dev.zio::zio-test:2.0.21" //> using dep "dev.zio::zio-json:0.6.2" //> using options "-Yretain-trees" // When case classes are using default values // --------------------- import zio.* import zio.json.* import zio.json.ast.{Json, JsonCursor, JsonType} import zio.json.ast.Json.* import zio.json.ast.JsonCursor.* import zio.test.* import zio.test.TestAspect.* import zio.test.Assertion.* import scala.annotation.targetName import java.time.{Instant, ZonedDateTime} import java.util.UUID case class A( message: String ) derives JsonCodec case class B( value: Long ) derives JsonCodec case class Something( a: Int, b: Int ) derives JsonCodec case class MayHaveContent( id: String, content: Option[String] ) derives JsonCodec case class SomethingComplex( a: Int, b: Int, c: MayHaveContent ) derives JsonCodec case class DummyClassWithURIBasedPropertyName( @targetName("isIn") `http://elite.polito.it/ontologies/dogont.owl#isIn`: String, @targetName("isbn1") `URN:ISBN:0-395-36341-1`: Int ) derives JsonCodec enum Gender(val code: Int) { case Male extends Gender(51) case Female extends Gender(42) } object Gender { given JsonEncoder[Gender] = JsonEncoder[String].contramap(p => p.toString) given JsonDecoder[Gender] = JsonDecoder[String].map(p => Gender.valueOf(p)) } object JsonTests extends ZIOSpecDefault: def spec = suite("learning zio json through tests")( // ----------------------------------------------------------------------- test("literal types")( assertTrue( "42".fromJson[Int] == Right(42), "42.0".fromJson[Double] == Right(42d), """"hello"""".fromJson[String] == Right("hello") ) ), // ----------------------------------------------------------------------- test("object types")( assertTrue( """{"a":42,"b":24}""".fromJson[Map[String, Int]] == Right(Map("a" -> 42, "b" -> 24)), """{"a":42,"b":24}""".fromJson[Something] == Right(Something(42, 24)) ) ), // ----------------------------------------------------------------------- test("collection types")( assertTrue( // "[1,2,3]".fromJson[Array[Int]] == Right(Array(1, 2, 3)), // TAKE CARE WITH JAVA ARRAY => THIS TEST FAILS "[1,2,3]".fromJson[List[Int]] == Right(List(1, 2, 3)), "[1,2,3]".fromJson[Vector[Int]] == Right(Vector(1, 2, 3)), """[{"a":42,"b":24}, {"a":52,"b":34}]""".fromJson[List[Something]] == Right(List(Something(42, 24), Something(52, 34))) ) ), // ----------------------------------------------------------------------- test("date/time types")( // assert(""""2021-12-30T10:57:55+01:00"""".fromJson[Instant])(isRight(equalTo(Instant.parse("2021-12-30T10:57:55+01:00")))) assertTrue( """"2021-12-30T09:57:55Z"""".fromJson[Instant] == Right(Instant.parse("2021-12-30T10:57:55+01:00")), """"2021-12-30T10:57:55+01:00"""".fromJson[ZonedDateTime] == Right(ZonedDateTime.parse("2021-12-30T10:57:55+01:00")) ) ), // ----------------------------------------------------------------------- test("advanced types")( assertTrue(""""da0214d8-88fe-4d3f-8fc4-bd1ac19758c1"""".fromJson[UUID] == Right(UUID.fromString("da0214d8-88fe-4d3f-8fc4-bd1ac19758c1"))) ), // ----------------------------------------------------------------------- test("enumeration types")( assertTrue( "\"Male\"".fromJson[Gender] == Right(Gender.Male), Gender.Female.toJson == "\"Female\"" ) ), // ----------------------------------------------------------------------- test ("jsonify") { val result = """{"a":42,"b":24}""" val collection = Map("a" -> 42, "b" -> 24) assertTrue( collection.toJson == result, Something(42, 24).toJson == result ) }, test("jsonify complex") { val result = """{"a":42,"b":24,"c":{"id":"aa-bb","content":"hello"}}""" val collection = Map("a" -> Num(42), "b" -> Num(24), "c" -> Obj("id" -> Str("aa-bb"), "content" -> Str("hello"))) assertTrue( collection.toJson == result, SomethingComplex(42, 24, MayHaveContent("aa-bb", Some("hello"))).toJson == result ) }, test("jsonify complex property name") { val result = """{"http://elite.polito.it/ontologies/dogont.owl#isIn":"room","URN:ISBN:0-395-36341-1":42}""" assertTrue( DummyClassWithURIBasedPropertyName("room", 42).toJson == result ) }, test("jsonify map") { val result = """{"a":42,"b":24.0,"c":"hello"}""" type GenericValue = Int | Double | String type GenericMap = Map[String, GenericValue] given JsonEncoder[GenericMap] = JsonEncoder[Map[String, Json]].contramap { initialMap => initialMap.map { case (key, x: Int) => key -> Num(x) case (key, x: Double) => key -> Num(x) case (key, x: String) => key -> Str(x) } } val collection: Map[String, GenericValue] = Map("a" -> 42, "b" -> 24d, "c" -> "hello") assertTrue(collection.toJson == result) }, test("jsonify map 2") { case class Dummy(x: Int, y: String) derives JsonCodec type GenericValue = Int | Double | String | Dummy type GenericMap = Map[String, GenericValue] given JsonEncoder[GenericMap] = JsonEncoder[Map[String, Json]].contramap { initialMap => initialMap.map { case (key, x: Int) => key -> Num(x) case (key, x: Double) => key -> Num(x) case (key, x: String) => key -> Str(x) case (key, x: Dummy) => key -> x.toJsonAST.toOption.get // OF COURSE VERY BAD CODE HERE // AND : the type test for Dummy cannot be checked at runtime } } val result = """{"a":42,"b":24.0,"c":"hello"}""" val collection: Map[String, GenericValue] = Map("a" -> 42, "b" -> 24d, "c" -> "hello") assertTrue(collection.toJson == result) }, // ----------------------------------------------------------------------- test("jsonify option") { val json1 = """{"id":"42"}""" val json2 = """{"id":"42","content":"the response"}""" val inst1 = MayHaveContent("42", None) val inst2 = MayHaveContent("42", Some("the response")) for { parsedInst1 <- ZIO.from(json1.fromJson[MayHaveContent]) parsedInst2 <- ZIO.from(json2.fromJson[MayHaveContent]) astJson1 <- ZIO.from(json1.fromJson[Json]) astJson2 <- ZIO.from(json2.fromJson[Json]) // convertedInst1 <- ZIO.from(astJson1.as[MayHaveContent]) convertedInst2 <- ZIO.from(astJson2.as[MayHaveContent]) } yield assertTrue( inst1.toJson == json1, inst2.toJson == json2, parsedInst1 == inst1, parsedInst2 == inst2, // convertedInst1 == inst1, convertedInst2 == inst2 ) }, // ----------------------------------------------------------------------- test("jsonify JWT example") { val jwtId = UUID.randomUUID().toString val nowEpochSeconds = Instant.now.getEpochSecond val result = s"""{ | "jti" : "$jwtId", | "iss" : "this-app", | "iat" : $nowEpochSeconds, | "exp" : ${nowEpochSeconds + 60L}, | "nbf" : ${nowEpochSeconds + 2L}, | "sub" : "userlogin@example.com", | "user" : 1 |}""".stripMargin val claim = Map( "jti" -> Str(jwtId), // JTW ID "iss" -> Str("this-app"), // Issuer "iat" -> Num(nowEpochSeconds), // Issued at "exp" -> Num(nowEpochSeconds + 60L), // Expiration time "nbf" -> Num(nowEpochSeconds + 2L), // Not before "sub" -> Str("userlogin@example.com"), // The subject "user" -> Num(1) ) for { claimAST <- ZIO.from(claim.toJsonAST) resultAST <- ZIO.from(result.fromJson[Json]) } yield assertTrue(claimAST == resultAST) }, // ----------------------------------------------------------------------- test("json AST") { val reference = """{"a":42,"b":24}""" for { result <- ZIO.from(reference.fromJson[Json]) } yield { assertTrue(result.toJson == reference) && assertTrue(result.as[Something] == Right(Something(42, 24))) && assert(result.as[Something])(isRight(equalTo(Something(42, 24)))) && assertTrue(result.as[Map[String, Int]] == Right(Map("a" -> 42, "b" -> 24))) && assert(result.as[Map[String, Int]])(isRight(equalTo(Map("a" -> 42, "b" -> 24)))) } }, // ----------------------------------------------------------------------- test("build json from scratch") { val json: Json = Obj("a" -> Num(42), "b" -> Num(24)) val payload = json.merge(Obj("c" -> Str("424"))) assertTrue(payload.toJson == """{"a":42,"b":24,"c":"424"}""") }, // ----------------------------------------------------------------------- test("build json from scala data structures simple case") { val json = Map("a" -> 42, "b" -> 24) for { payload <- ZIO.from(json.toJsonAST) // initially Either[String,Json] } yield assertTrue(payload.toJson == """{"a":42,"b":24}""") }, // ----------------------------------------------------------------------- // test("build json from scala data structures complex case") { // val json = Map("a" -> 42, "b" -> 24, "c" -> "424", "d" -> Map("x" -> 42)) // // Map[String, Any] => No codec for Any of course ! // for { // payload <- ZIO.from(json.toJsonAST) // } yield assertTrue(payload.toJson == """{"a":42,"b":24,"c":"424","d":{"x":42}}""") // }, // ----------------------------------------------------------------------- test("build json from scala data structures limitations") { // use Json instead of AnyRef As it encapsulate types val json: Map[String, Json] = Map( "a" -> Num(42), "b" -> Num(24), "c" -> Str("424"), "d" -> Obj("x" -> Num(42)) ) for { payload <- ZIO.from(json.toJsonAST) } yield assertTrue(payload.toJson == """{"a":42,"b":24,"c":"424","d":{"x":42}}""") }, // ----------------------------------------------------------------------- test("build json from scala data structures limitations alternative ?") { // type JMap = Map[String, JVal] // Cyclic reference ! // type JVal = String | Int | Double | JMap // val json: JMap = Map( // "a" -> 42, // "b" -> 24, // "c" -> "424", // "d" -> Map("x" -> 42d) // ) // case class is the only solution case class D(x: Double) derives JsonCodec case class O(a: Int, b: Int, c: String, d: D) derives JsonCodec val json = O(a = 42, b = 24, c = "424", d = D(x = 42d)) for { payload <- ZIO.from(json.toJsonAST) } yield assertTrue(payload.toJson == """{"a":42,"b":24,"c":"424","d":{"x":42.0}}""") }, // ----------------------------------------------------------------------- test("json AST content extraction") { val reference = """{"a":42,"b":24,"items":[42,24]}""" val jsonEither = reference.fromJson[Json] for { result <- ZIO.from(jsonEither) itemsJson <- ZIO.from(result.get(field("items").isArray)) items <- ZIO.from(itemsJson.as[List[Int]]) } yield { assertTrue(items == List(42, 24)) } }, // ----------------------------------------------------------------------- test("json AST default values for missing fields") { val reference = """{"a":42,"b":24,"c":{"c1":142,"c2":124}}""" for { result <- ZIO.from(reference.fromJson[Json]) rawArr1 <- ZIO.from(result.get(field("arr").isArray)).option.map(_.getOrElse(Arr())) emptyArray <- ZIO.from(rawArr1.as[List[Int]]) } yield { assertTrue(emptyArray.isEmpty) } }, // ----------------------------------------------------------------------- test("json AST content array field extraction") { val reference = """{ | "name":"joe", | "age":42, | "address":{"town":"there", "country":"france"}, | "phones":[{"kind":"mobile","num":"+330600000000"}, {"kind":"fix"}] |}""".stripMargin for { json <- ZIO.from(JsonDecoder[Json].decodeJson(reference)) cursor = field("phones").isArray.element(0).isObject.field("num").isString phoneJson <- ZIO.from(json.get(cursor)) // Str(phone) <- ZIO.from(json.get(cursor)).mapError(err => Exception(err)) // Str(phone2) <- ZIO.attempt(json.get(cursor)).absolve } yield { assertTrue( phoneJson.value == "+330600000000" // assertTrue(phone == "+330600000000" // assertTrue(phone2 == "+330600000000" ) } }, // ----------------------------------------------------------------------- test("json AST equality corner cases") { val a = """{"a":42}""" val b = """{"a":42.0}""" for { astA <- ZIO.from(a.fromJson[Json]) astB <- ZIO.from(b.fromJson[Json]) } yield assertTrue(astA == astB) } @@ ignore, // ----------------------------------------------------------------------- test("json encoding/decoding either type") { type MyEither = Either[String, Long] val a: MyEither = Right(42L) val b: MyEither = Left("Hello") for { // _ <- Console.printLine(s"a=${a.toJson} b=${b.toJson}") resultA <- ZIO.from(a.toJson.fromJson[MyEither]) resultB <- ZIO.from(b.toJson.fromJson[MyEither]) } yield assertTrue( resultA == a, resultB == b ) }, // ----------------------------------------------------------------------- test("json encoding/decoding either type with case classes") { type MyEither = Either[A, B] val a: MyEither = Right(B(42L)) val b: MyEither = Left(A("Hello")) for { // _ <- Console.printLine(s"a=${a.toJson} b=${b.toJson}") resultA <- ZIO.from(a.toJson.fromJson[MyEither]) resultB <- ZIO.from(b.toJson.fromJson[MyEither]) } yield assertTrue( resultA == a, resultB == b ) } ) JsonTests.main(Array.empty)