Skip to content

Commit e87083a

Browse files
committed
Fix remaining inconsistency in JSON codecs for Schema.CaseClass* and Schema.GenericRecord types
1 parent 3b49167 commit e87083a

File tree

2 files changed

+48
-37
lines changed

2 files changed

+48
-37
lines changed

zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,9 @@ object JsonCodec {
805805
case Json.Null => DynamicValue.NoneValue
806806
}
807807

808+
private def error(msg: String, trace: List[JsonError]): Nothing =
809+
throw UnsafeJson(JsonError.Message(msg) :: trace)
810+
808811
private def enumDecoder[Z](parentSchema: Schema.Enum[Z]): ZJsonDecoder[Z] = {
809812
val caseNameAliases = new mutable.HashMap[String, Schema.Case[Z, Any]]
810813
parentSchema.cases.foreach { case_ =>
@@ -813,9 +816,6 @@ object JsonCodec {
813816
case_.caseNameAliases.foreach(a => caseNameAliases.put(a, schema))
814817
}
815818

816-
def error(msg: String, trace: List[JsonError]): Nothing =
817-
throw UnsafeJson(JsonError.Message(msg) :: trace)
818-
819819
if (parentSchema.cases.forall(_.schema.isInstanceOf[Schema.CaseClass0[_]])) { // if all cases are CaseClass0, decode as String
820820
if (caseNameAliases.size <= 64) {
821821
new ZJsonDecoder[Z] {
@@ -973,15 +973,10 @@ object JsonCodec {
973973
val capacity = schema.fields.size * 2
974974
val spansWithDecoders =
975975
new util.HashMap[String, (JsonError.ObjectAccess, ZJsonDecoder[Any])](capacity)
976-
val defaults = new util.HashMap[String, Any](capacity)
977976
schema.fields.foreach { field =>
978-
val fieldName = field.fieldName
979977
val spanWithDecoder =
980-
(JsonError.ObjectAccess(fieldName), schemaDecoder(field.schema).asInstanceOf[ZJsonDecoder[Any]])
978+
(JsonError.ObjectAccess(field.fieldName), schemaDecoder(field.schema).asInstanceOf[ZJsonDecoder[Any]])
981979
field.nameAndAliases.foreach(x => spansWithDecoders.put(x, spanWithDecoder))
982-
if ((field.optional || field.transient) && field.defaultValue.isDefined) {
983-
defaults.put(fieldName, field.defaultValue.get)
984-
}
985980
}
986981
val skipExtraFields = !schema.annotations.exists(_.isInstanceOf[rejectExtraFields])
987982
(trace: List[JsonError], in: RetractReader) => {
@@ -1002,21 +997,33 @@ object JsonCodec {
1002997
lexer.char(trace_, in, ':')
1003998
val fieldName = span.field
1004999
val prev = map.put(fieldName, dec.unsafeDecode(trace_, in))
1005-
if (prev != null) {
1006-
throw UnsafeJson(JsonError.Message("duplicate") :: trace_)
1007-
}
1000+
if (prev != null) error("duplicate", trace_)
10081001
} else if (skipExtraFields || discriminator.contains(fieldNameOrAlias)) {
10091002
lexer.char(trace, in, ':')
10101003
lexer.skipValue(trace, in)
1011-
} else {
1012-
throw UnsafeJson(JsonError.Message(s"unexpected field: $fieldNameOrAlias") :: trace)
1013-
}
1004+
} else error("extra field", trace)
10141005
continue = lexer.nextField(trace, in)
10151006
}
1016-
val it = defaults.entrySet().iterator()
1017-
while (it.hasNext) {
1018-
val entry = it.next()
1019-
map.putIfAbsent(entry.getKey, entry.getValue)
1007+
schema.fields.foreach { field =>
1008+
map.computeIfAbsent(
1009+
field.fieldName,
1010+
fieldName => {
1011+
if ((field.optional || field.transient) && field.defaultValue.isDefined) {
1012+
field.defaultValue.get
1013+
} else {
1014+
var schema = field.schema
1015+
schema match {
1016+
case l: Schema.Lazy[_] => schema = l.schema
1017+
case _ =>
1018+
}
1019+
schema match {
1020+
case _: Schema.Optional[_] => None
1021+
case collection: Schema.Collection[_, _] => collection.empty
1022+
case _ => error("missing", spansWithDecoders.get(fieldName)._1 :: trace)
1023+
}
1024+
}
1025+
}
1026+
)
10201027
}
10211028
(ListMap.newBuilder[String, Any] ++= ({ // to avoid O(n) insert operations
10221029
import scala.collection.JavaConverters.mapAsScalaMapConverter // use deprecated class for Scala 2.12 compatibility
@@ -1095,10 +1102,7 @@ object JsonCodec {
10951102
case (Some(a), Some(b)) => Fallback.Both(a, b)
10961103
case (Some(a), _) => Fallback.Left(a)
10971104
case (_, Some(b)) => Fallback.Right(b)
1098-
case _ =>
1099-
throw UnsafeJson(
1100-
JsonError.Message("Fallback decoder was unable to decode both left and right sides") :: trace
1101-
)
1105+
case _ => error("Fallback decoder was unable to decode both left and right sides", trace)
11021106
}
11031107
}
11041108
}

zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,9 @@ object JsonCodecSpec extends ZIOSpecDefault {
477477
test("Do not encode transient field") {
478478
assertEncodes(
479479
RecordExample.schema,
480-
RecordExample(f1 = Some("test"), f3 = Some("transient")),
480+
RecordExample(f1 = "test", f3 = Some("transient"), f20 = None, f21 = Vector.empty, f22 = Nil),
481481
charSequenceToByteChunk(
482-
"""{"$f1":"test"}""".stripMargin
482+
"""{"$f1":"test","f20":null,"f21":[],"f22":[]}""".stripMargin
483483
)
484484
)
485485
}
@@ -732,26 +732,33 @@ object JsonCodecSpec extends ZIOSpecDefault {
732732
charSequenceToByteChunk("""{"foo":"s","bar":1,"baz":2}""")
733733
)
734734
},
735-
test("with missing fields") {
735+
test("with empty optional and collection fields without default values") {
736736
assertDecodes(
737737
RecordExample.schema,
738-
RecordExample(f1 = Some("test"), f2 = None),
738+
RecordExample(f1 = "test", f2 = None, f20 = None, f21 = Vector.empty, f22 = Nil),
739739
charSequenceToByteChunk("""{"$f1":"test"}""")
740740
)
741741
},
742+
test("missing required fields") {
743+
assertDecodesToError(
744+
RecordExample.schema,
745+
"""{}""",
746+
JsonError.Message("missing") :: JsonError.ObjectAccess("$f1") :: Nil
747+
)
748+
},
742749
test("aliased field") {
743750
assertDecodes(
744751
RecordExample.schema,
745-
RecordExample(f1 = Some("test"), f2 = Some("alias")),
752+
RecordExample(f1 = "test", f2 = Some("alias"), f20 = None, f21 = Vector.empty, f22 = Nil),
746753
charSequenceToByteChunk("""{"$f1":"test", "field2":"alias"}""")
747754
)
748755
},
749756
test("reject extra fields") {
750757
assertDecodes(
751758
RecordExample.schema.annotate(rejectExtraFields()),
752-
RecordExample(f1 = Some("test")),
759+
RecordExample(f1 = "test", f20 = None, f21 = Vector.empty, f22 = Nil),
753760
charSequenceToByteChunk("""{"$f1":"test", "extraField":"extra"}""")
754-
).flip.map(err => assertTrue(err.getMessage() == "(unexpected field: extraField)"))
761+
).flip.map(err => assertTrue(err.getMessage() == "(extra field)"))
755762
},
756763
test("reject duplicated fields") {
757764
assertDecodesToError(
@@ -2477,7 +2484,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
24772484
) extends OneOf4
24782485

24792486
case class RecordExample(
2480-
@fieldName("$f1") f1: Option[String], // the only field that does not have a default value
2487+
@fieldName("$f1") f1: String, // the only field that does not have a default value
24812488
@fieldNameAliases("field2") f2: Option[String] = None,
24822489
@transientField f3: Option[String] = None,
24832490
f4: Option[String] = None,
@@ -2493,12 +2500,12 @@ object JsonCodecSpec extends ZIOSpecDefault {
24932500
f14: Option[String] = None,
24942501
f15: Option[String] = None,
24952502
f16: Option[String] = None,
2496-
f17: Option[String] = None,
2497-
f18: Option[String] = None,
2498-
f19: Option[String] = None,
2499-
f20: Option[String] = None,
2500-
f21: Option[String] = None,
2501-
f22: Option[String] = None,
2503+
f17: List[String] = Nil,
2504+
f18: Vector[String] = Vector.empty,
2505+
f19: Option[RecordExample] = None,
2506+
f20: Option[String],
2507+
f21: Vector[String],
2508+
f22: List[String],
25022509
@fieldName("$f23") f23: Option[String] = None
25032510
)
25042511

0 commit comments

Comments
 (0)