Skip to content

Commit

Permalink
Fix remaining inconsistency in JSON codecs for Schema.CaseClass* an…
Browse files Browse the repository at this point in the history
…d `Schema.GenericRecord` types
  • Loading branch information
plokhotnyuk committed Jan 9, 2025
1 parent 3b49167 commit e87083a
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,9 @@ object JsonCodec {
case Json.Null => DynamicValue.NoneValue
}

private def error(msg: String, trace: List[JsonError]): Nothing =
throw UnsafeJson(JsonError.Message(msg) :: trace)

private def enumDecoder[Z](parentSchema: Schema.Enum[Z]): ZJsonDecoder[Z] = {
val caseNameAliases = new mutable.HashMap[String, Schema.Case[Z, Any]]
parentSchema.cases.foreach { case_ =>
Expand All @@ -813,9 +816,6 @@ object JsonCodec {
case_.caseNameAliases.foreach(a => caseNameAliases.put(a, schema))
}

def error(msg: String, trace: List[JsonError]): Nothing =
throw UnsafeJson(JsonError.Message(msg) :: trace)

if (parentSchema.cases.forall(_.schema.isInstanceOf[Schema.CaseClass0[_]])) { // if all cases are CaseClass0, decode as String
if (caseNameAliases.size <= 64) {
new ZJsonDecoder[Z] {
Expand Down Expand Up @@ -973,15 +973,10 @@ object JsonCodec {
val capacity = schema.fields.size * 2
val spansWithDecoders =
new util.HashMap[String, (JsonError.ObjectAccess, ZJsonDecoder[Any])](capacity)
val defaults = new util.HashMap[String, Any](capacity)
schema.fields.foreach { field =>
val fieldName = field.fieldName
val spanWithDecoder =
(JsonError.ObjectAccess(fieldName), schemaDecoder(field.schema).asInstanceOf[ZJsonDecoder[Any]])
(JsonError.ObjectAccess(field.fieldName), schemaDecoder(field.schema).asInstanceOf[ZJsonDecoder[Any]])
field.nameAndAliases.foreach(x => spansWithDecoders.put(x, spanWithDecoder))
if ((field.optional || field.transient) && field.defaultValue.isDefined) {
defaults.put(fieldName, field.defaultValue.get)
}
}
val skipExtraFields = !schema.annotations.exists(_.isInstanceOf[rejectExtraFields])
(trace: List[JsonError], in: RetractReader) => {
Expand All @@ -1002,21 +997,33 @@ object JsonCodec {
lexer.char(trace_, in, ':')
val fieldName = span.field
val prev = map.put(fieldName, dec.unsafeDecode(trace_, in))
if (prev != null) {
throw UnsafeJson(JsonError.Message("duplicate") :: trace_)
}
if (prev != null) error("duplicate", trace_)
} else if (skipExtraFields || discriminator.contains(fieldNameOrAlias)) {
lexer.char(trace, in, ':')
lexer.skipValue(trace, in)
} else {
throw UnsafeJson(JsonError.Message(s"unexpected field: $fieldNameOrAlias") :: trace)
}
} else error("extra field", trace)
continue = lexer.nextField(trace, in)
}
val it = defaults.entrySet().iterator()
while (it.hasNext) {
val entry = it.next()
map.putIfAbsent(entry.getKey, entry.getValue)
schema.fields.foreach { field =>
map.computeIfAbsent(
field.fieldName,
fieldName => {
if ((field.optional || field.transient) && field.defaultValue.isDefined) {
field.defaultValue.get
} else {
var schema = field.schema
schema match {
case l: Schema.Lazy[_] => schema = l.schema
case _ =>
}
schema match {
case _: Schema.Optional[_] => None
case collection: Schema.Collection[_, _] => collection.empty
case _ => error("missing", spansWithDecoders.get(fieldName)._1 :: trace)
}
}
}
)
}
(ListMap.newBuilder[String, Any] ++= ({ // to avoid O(n) insert operations
import scala.collection.JavaConverters.mapAsScalaMapConverter // use deprecated class for Scala 2.12 compatibility
Expand Down Expand Up @@ -1095,10 +1102,7 @@ object JsonCodec {
case (Some(a), Some(b)) => Fallback.Both(a, b)
case (Some(a), _) => Fallback.Left(a)
case (_, Some(b)) => Fallback.Right(b)
case _ =>
throw UnsafeJson(
JsonError.Message("Fallback decoder was unable to decode both left and right sides") :: trace
)
case _ => error("Fallback decoder was unable to decode both left and right sides", trace)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,9 @@ object JsonCodecSpec extends ZIOSpecDefault {
test("Do not encode transient field") {
assertEncodes(
RecordExample.schema,
RecordExample(f1 = Some("test"), f3 = Some("transient")),
RecordExample(f1 = "test", f3 = Some("transient"), f20 = None, f21 = Vector.empty, f22 = Nil),
charSequenceToByteChunk(
"""{"$f1":"test"}""".stripMargin
"""{"$f1":"test","f20":null,"f21":[],"f22":[]}""".stripMargin
)
)
}
Expand Down Expand Up @@ -732,26 +732,33 @@ object JsonCodecSpec extends ZIOSpecDefault {
charSequenceToByteChunk("""{"foo":"s","bar":1,"baz":2}""")
)
},
test("with missing fields") {
test("with empty optional and collection fields without default values") {
assertDecodes(
RecordExample.schema,
RecordExample(f1 = Some("test"), f2 = None),
RecordExample(f1 = "test", f2 = None, f20 = None, f21 = Vector.empty, f22 = Nil),
charSequenceToByteChunk("""{"$f1":"test"}""")
)
},
test("missing required fields") {
assertDecodesToError(
RecordExample.schema,
"""{}""",
JsonError.Message("missing") :: JsonError.ObjectAccess("$f1") :: Nil
)
},
test("aliased field") {
assertDecodes(
RecordExample.schema,
RecordExample(f1 = Some("test"), f2 = Some("alias")),
RecordExample(f1 = "test", f2 = Some("alias"), f20 = None, f21 = Vector.empty, f22 = Nil),
charSequenceToByteChunk("""{"$f1":"test", "field2":"alias"}""")
)
},
test("reject extra fields") {
assertDecodes(
RecordExample.schema.annotate(rejectExtraFields()),
RecordExample(f1 = Some("test")),
RecordExample(f1 = "test", f20 = None, f21 = Vector.empty, f22 = Nil),
charSequenceToByteChunk("""{"$f1":"test", "extraField":"extra"}""")
).flip.map(err => assertTrue(err.getMessage() == "(unexpected field: extraField)"))
).flip.map(err => assertTrue(err.getMessage() == "(extra field)"))
},
test("reject duplicated fields") {
assertDecodesToError(
Expand Down Expand Up @@ -2477,7 +2484,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
) extends OneOf4

case class RecordExample(
@fieldName("$f1") f1: Option[String], // the only field that does not have a default value
@fieldName("$f1") f1: String, // the only field that does not have a default value
@fieldNameAliases("field2") f2: Option[String] = None,
@transientField f3: Option[String] = None,
f4: Option[String] = None,
Expand All @@ -2493,12 +2500,12 @@ object JsonCodecSpec extends ZIOSpecDefault {
f14: Option[String] = None,
f15: Option[String] = None,
f16: Option[String] = None,
f17: Option[String] = None,
f18: Option[String] = None,
f19: Option[String] = None,
f20: Option[String] = None,
f21: Option[String] = None,
f22: Option[String] = None,
f17: List[String] = Nil,
f18: Vector[String] = Vector.empty,
f19: Option[RecordExample] = None,
f20: Option[String],
f21: Vector[String],
f22: List[String],
@fieldName("$f23") f23: Option[String] = None
)

Expand Down

0 comments on commit e87083a

Please sign in to comment.