Skip to content

Commit 097d4ed

Browse files
authored
Add body json en-/decoding (#2287) (#3731)
1 parent 6d977a0 commit 097d4ed

File tree

3 files changed

+366
-84
lines changed

3 files changed

+366
-84
lines changed

docs/reference/body/body.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,50 @@ val body = Body.from(person)
119119

120120
In the above example, we used a JSON codec to encode the person object into a body. Similarly, we can use other codecs like Avro, Protobuf, etc.
121121

122+
### From JSON
123+
124+
ZIO HTTP provides convenient methods for creating a `Body` from JSON data using either **zio-schema** or **zio-json** libraries.
125+
126+
#### Using ZIO Schema
127+
128+
To create a JSON body using zio-schema, use the `Body.json` method with an implicit `Schema`:
129+
130+
```scala mdoc:compile-only
131+
import zio.schema.{DeriveSchema, Schema}
132+
133+
case class Person(name: String, age: Int)
134+
implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
135+
136+
val person = Person("John", 42)
137+
val body = Body.json(person)
138+
```
139+
140+
This method automatically:
141+
- Encodes the value to JSON using zio-schema's JSON codec
142+
- Sets the `Content-Type` header to `application/json`
143+
144+
#### Using ZIO JSON
145+
146+
Alternatively, you can create a JSON body using zio-json with the `Body.jsonCodec` method:
147+
148+
```scala mdoc:compile-only
149+
import zio.json.{DeriveJsonEncoder, JsonEncoder}
150+
151+
case class Person(name: String, age: Int)
152+
implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person]
153+
154+
val person = Person("John", 42)
155+
val body = Body.jsonCodec(person)
156+
```
157+
158+
This method:
159+
- Encodes the value to JSON using zio-json's encoder
160+
- Sets the `Content-Type` header to `application/json`
161+
162+
**When to use which?**
163+
- Use `Body.json` (zio-schema) when you're already using zio-schema in your project or need advanced schema features
164+
- Use `Body.jsonCodec` (zio-json) when you prefer zio-json's dedicated JSON library or need fine-grained control over JSON encoding
165+
122166
### From ZIO Streams
123167

124168
There are several ways to create a `Body` from a ZIO Stream:
@@ -346,6 +390,70 @@ val body = Body.from(person)
346390
val decodedPerson = body.to[Person]
347391
```
348392

393+
### Decoding JSON Body Content
394+
395+
ZIO HTTP provides convenient methods for decoding JSON body content using either **zio-schema** or **zio-json** libraries.
396+
397+
#### Using ZIO Schema
398+
399+
To decode a JSON body using zio-schema, use the `Body#asJson` method with an implicit `Schema`:
400+
401+
```scala mdoc:compile-only
402+
import zio.schema.{DeriveSchema, Schema}
403+
404+
case class Person(name: String, age: Int)
405+
implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
406+
407+
val jsonBody = Body.fromString("""{"name":"John","age":42}""")
408+
val decodedPerson: Task[Person] = jsonBody.asJson[Person]
409+
```
410+
411+
This method:
412+
- Decodes the JSON body content using zio-schema's JSON codec
413+
- Returns a `Task[A]` that will fail if the JSON is invalid or doesn't match the schema
414+
415+
#### Using ZIO JSON
416+
417+
Alternatively, you can decode a JSON body using zio-json with the `Body#asJsonFromCodec` method:
418+
419+
```scala mdoc:compile-only
420+
import zio.json.{DeriveJsonDecoder, JsonDecoder}
421+
422+
case class Person(name: String, age: Int)
423+
implicit val decoder: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person]
424+
425+
val jsonBody = Body.fromString("""{"name":"John","age":42}""")
426+
val decodedPerson: Task[Person] = jsonBody.asJsonFromCodec[Person]
427+
```
428+
429+
This method:
430+
- Decodes the JSON body content using zio-json's decoder
431+
- Returns a `Task[A]` that will fail if the JSON is invalid or cannot be decoded
432+
433+
**When to use which?**
434+
- Use `asJson` (zio-schema) when you're already using zio-schema in your project or need schema validation
435+
- Use `asJsonFromCodec` (zio-json) when you prefer zio-json's dedicated JSON library or need fine-grained control over JSON decoding
436+
437+
**Round-trip Example:**
438+
439+
Here's a complete example showing encoding and decoding with zio-schema:
440+
441+
```scala mdoc:compile-only
442+
import zio.schema.{DeriveSchema, Schema}
443+
444+
case class Person(name: String, age: Int)
445+
implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
446+
447+
val program = for {
448+
// Create a JSON body
449+
person <- ZIO.succeed(Person("Alice", 30))
450+
body = Body.json(person)
451+
452+
// Decode it back
453+
decoded <- body.asJson[Person]
454+
} yield decoded
455+
```
456+
349457
### Retrieving Raw Body Content
350458

351459
We can access the content of the body as an array of bytes or a chunk of bytes. This is useful when dealing with binary data. Here's how you can do it:

zio-http/jvm/src/test/scala/zio/http/BodySpec.scala

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,27 @@ package zio.http
1818

1919
import java.io.File
2020

21+
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}
2122
import zio.test.Assertion.equalTo
2223
import zio.test.TestAspect.timeout
2324
import zio.test._
2425
import zio.{Scope, durationInt}
2526

2627
import zio.stream.ZStream
2728

29+
import zio.schema.{DeriveSchema, Schema}
30+
2831
object BodySpec extends ZIOHttpSpec {
2932
private val testFile = new File(getClass.getResource("/TestFile.txt").getPath)
3033

34+
case class Person(name: String, age: Int)
35+
36+
object Person {
37+
implicit val schema: Schema[Person] = DeriveSchema.gen[Person]
38+
implicit val encoder: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person]
39+
implicit val decoder: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person]
40+
}
41+
3142
override def spec: Spec[TestEnvironment with Scope, Throwable] =
3243
suite("BodySpec")(
3344
suite("outgoing")(
@@ -56,6 +67,93 @@ object BodySpec extends ZIOHttpSpec {
5667
),
5768
),
5869
),
70+
suite("json")(
71+
suite("Body.json with zio-schema")(
72+
test("creates JSON body from case class") {
73+
val person = Person("John", 42)
74+
val body = Body.json(person)
75+
for {
76+
content <- body.asString
77+
} yield assertTrue(
78+
content.contains("John"),
79+
content.contains("42"),
80+
body.mediaType.contains(MediaType.application.json),
81+
)
82+
},
83+
test("round-trip encoding and decoding") {
84+
val person = Person("Alice", 30)
85+
val body = Body.json(person)
86+
for {
87+
decoded <- body.asJson[Person]
88+
} yield assertTrue(decoded == person)
89+
},
90+
),
91+
suite("Body.json with zio-json")(
92+
test("creates JSON body from case class") {
93+
val person = Person("Jane", 25)
94+
val body = Body.jsonCodec(person)
95+
for {
96+
content <- body.asString
97+
} yield assertTrue(
98+
content.contains("Jane"),
99+
content.contains("25"),
100+
body.mediaType.contains(MediaType.application.json),
101+
)
102+
},
103+
test("round-trip encoding and decoding") {
104+
val person = Person("Bob", 35)
105+
val body = Body.jsonCodec(person)
106+
for {
107+
decoded <- body.asJsonFromCodec[Person]
108+
} yield assertTrue(decoded == person)
109+
},
110+
),
111+
suite("Body.asJson with zio-schema")(
112+
test("decodes JSON string to case class") {
113+
val jsonString = """{"name":"Charlie","age":28}"""
114+
val body = Body.fromString(jsonString)
115+
for {
116+
decoded <- body.asJson[Person]
117+
} yield assertTrue(
118+
decoded.name == "Charlie",
119+
decoded.age == 28,
120+
)
121+
},
122+
test("fails on invalid JSON") {
123+
val invalidJson = """{"name":"Invalid"}"""
124+
val body = Body.fromString(invalidJson)
125+
for {
126+
result <- body.asJson[Person].exit
127+
} yield assertTrue(result.isFailure)
128+
},
129+
),
130+
suite("Body.asJsonZio with zio-json")(
131+
test("decodes JSON string to case class") {
132+
val jsonString = """{"name":"David","age":40}"""
133+
val body = Body.fromString(jsonString)
134+
for {
135+
decoded <- body.asJsonFromCodec[Person]
136+
} yield assertTrue(
137+
decoded.name == "David",
138+
decoded.age == 40,
139+
)
140+
},
141+
test("fails on invalid JSON") {
142+
val invalidJson = """{"invalid":"json"}"""
143+
val body = Body.fromString(invalidJson)
144+
for {
145+
result <- body.asJsonFromCodec[Person].exit
146+
} yield assertTrue(result.isFailure)
147+
},
148+
test("fails on malformed JSON") {
149+
val malformedJson = """not valid json"""
150+
val body = Body.fromString(malformedJson)
151+
for {
152+
result <- body.asJsonFromCodec[Person].exit
153+
} yield assertTrue(result.isFailure)
154+
},
155+
),
156+
),
59157
suite("mediaType")(
60158
test("updates the Body media type with the provided value") {
61159
val body = Body.fromString("test").contentType(MediaType.text.plain)

0 commit comments

Comments
 (0)