Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active January 6, 2024 11:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/320bc600ab3d8def8c915af096974e1b to your computer and use it in GitHub Desktop.
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/40cadbd8aa99c0c3de770cd58ab647a0f736e0b8
// 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.3.1"
//> 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)
@hardlianotion
Copy link

Hey thanks for the cheatsheet. I used it to get more familiar with the ast cursor functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment