Skip to content

Commit 330af66

Browse files
committed
Support null values in json encoding
1 parent 9345980 commit 330af66

2 files changed

Lines changed: 73 additions & 63 deletions

File tree

json/src/main/scala/proteus/json/package.scala

Lines changed: 65 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,72 +26,74 @@ implicit def jsonWriterCodec[A](using codec: ProtobufCodec[A], registry: Registr
2626
def apply(a: A): Json =
2727
withRegisters { registers =>
2828
def loop[B](b: B, codec: ProtobufCodec[B], offset: RegisterOffset): Json =
29-
registry.get(codec) match {
30-
case null =>
31-
codec match {
32-
case c: Primitive[_] =>
33-
c.primitiveType match {
34-
case _: PrimitiveType.Int => Json.fromInt(b)
35-
case _: PrimitiveType.Long => Json.fromLong(b)
36-
case _: PrimitiveType.Boolean => Json.fromBoolean(b)
37-
case _: PrimitiveType.String => Json.fromString(b)
38-
case _: PrimitiveType.Double => Json.fromDoubleOrNull(b)
39-
case _: PrimitiveType.Float => Json.fromFloatOrNull(b)
40-
case _ => throw new Exception(s"Unsupported primitive type: ${c.primitiveType}")
41-
}
42-
case c: Enum[_] => Json.fromString(c.namesByValue(b))
43-
case c: Message[_] =>
44-
c.deconstructor.deconstruct(registers, offset, b)
45-
val nextOffset = RegisterOffset.add(offset, c.usedRegisters)
46-
val builder = List.newBuilder[(String, Json)]
47-
var i = 0
48-
while (i < c.fields.length) {
49-
val field = c.fields(i) match {
50-
case f: SimpleField[?] => Some(f)
51-
case f: OneofField[b] =>
52-
val v = getFromRegister(registers, offset, f.register).asInstanceOf[b]
53-
Some(f.cases(f.discriminator.discriminate(v)))
54-
case _: ExcludedField[?] => None
29+
if (b == null) Json.Null
30+
else
31+
registry.get(codec) match {
32+
case null =>
33+
codec match {
34+
case c: Primitive[_] =>
35+
c.primitiveType match {
36+
case _: PrimitiveType.Int => Json.fromInt(b)
37+
case _: PrimitiveType.Long => Json.fromLong(b)
38+
case _: PrimitiveType.Boolean => Json.fromBoolean(b)
39+
case _: PrimitiveType.String => Json.fromString(b)
40+
case _: PrimitiveType.Double => Json.fromDoubleOrNull(b)
41+
case _: PrimitiveType.Float => Json.fromFloatOrNull(b)
42+
case _ => throw new Exception(s"Unsupported primitive type: ${c.primitiveType}")
5543
}
56-
field.foreach { f =>
44+
case c: Enum[_] => Json.fromString(c.namesByValue(b))
45+
case c: Message[_] =>
46+
c.deconstructor.deconstruct(registers, offset, b)
47+
val nextOffset = RegisterOffset.add(offset, c.usedRegisters)
48+
val builder = List.newBuilder[(String, Json)]
49+
var i = 0
50+
while (i < c.fields.length) {
51+
val field = c.fields(i) match {
52+
case f: SimpleField[?] => Some(f)
53+
case f: OneofField[b] =>
54+
val v = getFromRegister(registers, offset, f.register).asInstanceOf[b]
55+
Some(f.cases(f.discriminator.discriminate(v)))
56+
case _: ExcludedField[?] => None
57+
}
58+
field.foreach { f =>
59+
builder +=
60+
toCamelCase(f.name) ->
61+
loop(getFromRegister(registers, offset, f.register).asInstanceOf[f.codec.Focus], f.codec, nextOffset)
62+
}
63+
i += 1
64+
}
65+
Json.obj(builder.result()*)
66+
case c: Repeated[c, e] =>
67+
val it = c.deconstructor.deconstruct[e](b)
68+
val builder = List.newBuilder[Json]
69+
while (it.hasNext) {
70+
val v = it.next
71+
builder += loop(v, c.element, offset)
72+
}
73+
Json.arr(builder.result()*)
74+
case c: RepeatedMap[m, k, v] =>
75+
val it = c.deconstructor.deconstruct(b.asInstanceOf[m[k, v]])
76+
val builder = List.newBuilder[Json]
77+
while (it.hasNext) {
78+
val v = it.next
5779
builder +=
58-
toCamelCase(f.name) ->
59-
loop(getFromRegister(registers, offset, f.register).asInstanceOf[f.codec.Focus], f.codec, nextOffset)
80+
Json.obj(
81+
loop(c.deconstructor.getKey(v), c.element.fields(0).asInstanceOf[SimpleField[?]].codec, offset).asString.getOrElse("") ->
82+
loop(c.deconstructor.getValue(v), c.element.fields(1).asInstanceOf[SimpleField[?]].codec, offset)
83+
)
84+
}
85+
Json.arr(builder.result()*)
86+
case Bytes => Json.fromString("<bytes>")
87+
case c: Transform[_, _] => loop(c.to(b), c.codec, offset)
88+
case c: RecursiveMessage[_] => loop(b, c.codec, offset)
89+
case c: Optional[_] =>
90+
b match {
91+
case None => Json.Null
92+
case Some(v) => loop(v, c.codec, offset)
6093
}
61-
i += 1
62-
}
63-
Json.obj(builder.result()*)
64-
case c: Repeated[c, e] =>
65-
val it = c.deconstructor.deconstruct[e](b)
66-
val builder = List.newBuilder[Json]
67-
while (it.hasNext) {
68-
val v = it.next
69-
builder += loop(v, c.element, offset)
70-
}
71-
Json.arr(builder.result()*)
72-
case c: RepeatedMap[m, k, v] =>
73-
val it = c.deconstructor.deconstruct(b.asInstanceOf[m[k, v]])
74-
val builder = List.newBuilder[Json]
75-
while (it.hasNext) {
76-
val v = it.next
77-
builder +=
78-
Json.obj(
79-
loop(c.deconstructor.getKey(v), c.element.fields(0).asInstanceOf[SimpleField[?]].codec, offset).asString.getOrElse("") ->
80-
loop(c.deconstructor.getValue(v), c.element.fields(1).asInstanceOf[SimpleField[?]].codec, offset)
81-
)
82-
}
83-
Json.arr(builder.result()*)
84-
case Bytes => Json.fromString("<bytes>")
85-
case c: Transform[_, _] => loop(c.to(b), c.codec, offset)
86-
case c: RecursiveMessage[_] => loop(b, c.codec, offset)
87-
case c: Optional[_] =>
88-
b match {
89-
case None => Json.Null
90-
case Some(v) => loop(v, c.codec, offset)
91-
}
92-
}
93-
case encoder => encoder(b)
94-
}
94+
}
95+
case encoder => encoder(b)
96+
}
9597

9698
loop(a, codec, RegisterOffset.Zero)
9799
}

json/src/test/scala/proteus/json/JsonSpec.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,14 @@ object JsonSpec extends ZIOSpecDefault {
292292

293293
assertTrue(result == """{"intVal":0,"stringVal":"","boolVal":false}""")
294294
},
295+
test("toJson handles null values") {
296+
case class B(c: Int) derives Schema, ProtobufCodec
297+
case class A(b: B) derives Schema, ProtobufCodec
298+
val instance = A(null)
299+
val result = instance.asJson.noSpaces
300+
301+
assertTrue(result == """{"b":null}""")
302+
},
295303
test("toJson handles complex nested structure") {
296304
case class Address(street: String, city: String, zip: Option[String]) derives Schema, ProtobufCodec
297305
case class Contact(email: String, phones: List[String]) derives Schema, ProtobufCodec

0 commit comments

Comments
 (0)