Skip to content

Commit e1759e3

Browse files
authored
Fix duplicated field rejection for JSON codecs of records (#765)
1 parent ffe4808 commit e1759e3

File tree

2 files changed

+34
-2
lines changed

2 files changed

+34
-2
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ object JsonCodec {
10021002
val rejectAdditionalFields = schema.annotations.exists(_.isInstanceOf[rejectExtraFields])
10031003
(trace: List[JsonError], in: RetractReader) => {
10041004
Lexer.char(trace, in, '{')
1005-
val map = defaults.clone().asInstanceOf[util.HashMap[String, Any]]
1005+
val map = new util.HashMap[String, Any]
10061006
var continue = Lexer.firstField(trace, in)
10071007
while (continue) {
10081008
val fieldNameOrAlias = Lexer.string(trace, in).toString
@@ -1013,7 +1013,10 @@ object JsonCodec {
10131013
val trace_ = span :: trace
10141014
Lexer.char(trace_, in, ':')
10151015
val fieldName = span.field
1016-
map.put(fieldName, dec.unsafeDecode(trace_, in))
1016+
val prev = map.put(fieldName, dec.unsafeDecode(trace_, in))
1017+
if (prev != null) {
1018+
throw UnsafeJson(JsonError.Message("duplicate") :: trace)
1019+
}
10171020
} else if (rejectAdditionalFields) {
10181021
throw UnsafeJson(JsonError.Message(s"unexpected field: $fieldNameOrAlias") :: trace)
10191022
} else {
@@ -1022,6 +1025,11 @@ object JsonCodec {
10221025
}
10231026
continue = Lexer.nextField(trace, in)
10241027
}
1028+
val it = defaults.entrySet().iterator()
1029+
while (it.hasNext) {
1030+
val entry = it.next()
1031+
map.putIfAbsent(entry.getKey, entry.getValue)
1032+
}
10251033
(ListMap.newBuilder[String, Any] ++= ({ // to avoid O(n) insert operations
10261034
import scala.collection.JavaConverters.mapAsScalaMapConverter // use deprecated class for Scala 2.12 compatibility
10271035

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,18 @@ object JsonCodecSpec extends ZIOSpecDefault {
708708
charSequenceToByteChunk("""{"$f1":"test", "extraField":"extra"}""")
709709
).flip.map(err => assertTrue(err.getMessage() == "(unexpected field: extraField)"))
710710
},
711+
test("reject duplicated fields") {
712+
assertDecodesToError(
713+
recordSchema,
714+
"""{"foo":"test","bar":10,"foo":10}""",
715+
JsonError.Message("duplicate") :: Nil
716+
) &>
717+
assertDecodesToError(
718+
recordSchema,
719+
"""{"bar":10,"foo":"test","bar":"100"}""",
720+
JsonError.Message("duplicate") :: Nil
721+
)
722+
},
711723
test("optional field with schema or annotated default value") {
712724
assertDecodes(
713725
RecordExampleWithOptField.schema,
@@ -812,6 +824,18 @@ object JsonCodecSpec extends ZIOSpecDefault {
812824
JsonError.Message("extra field") :: Nil
813825
)
814826
},
827+
test("reject duplicated fields") {
828+
assertDecodesToError(
829+
personSchema,
830+
"""{"name":"test","age":10,"name":10}""",
831+
JsonError.Message("duplicate") :: Nil
832+
) &>
833+
assertDecodesToError(
834+
personSchema,
835+
"""{"age":10,"name":"test","age":"100"}""",
836+
JsonError.Message("duplicate") :: Nil
837+
)
838+
},
815839
test("transient field annotation with default value in class definition") {
816840
assertDecodes(
817841
searchRequestWithTransientFieldSchema,

0 commit comments

Comments
 (0)