Last active
March 30, 2025 08:17
-
-
Save dacr/320bc600ab3d8def8c915af096974e1b to your computer and use it in GitHub Desktop.
ZIO learning - playing with json - zio-json cheat sheet / published by https://github.com/dacr/code-examples-manager #862c2592-c58c-4541-817b-eaf9da4c762e/8f61a19ede84238b6a1614dbdcf2aed8cfac5751
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 : ZIO learning - playing with json - zio-json cheat sheet | |
// keywords : scala, zio, learning, json, pure-functional, @testable | |
// 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) | |
// 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" : "[email protected]", | |
| "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("[email protected]"), // 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey thanks for the cheatsheet. I used it to get more familiar with the ast cursor functionality.