Skip to content

Commit e369dd4

Browse files
authored
Fix incorrect behavior in lenient tagged union decoders (#1620)
* Fix incorrect behavior in lenient tagged union decoders * Rearrange things + add entry in CHANGELOG
1 parent 31e6188 commit e369dd4

File tree

3 files changed

+92
-90
lines changed

3 files changed

+92
-90
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Thank you!
1111
* Adds utility types for working with endpoint handlers (see [#1612](https://github.com/disneystreaming/smithy4s/pull/1612))
1212
* Add a more informative error message for repeated namespaces (see [#1608](https://github.com/disneystreaming/smithy4s/pull/1608)).
1313
* Adds `com.disneystreaming.smithy4s:smithy4s-protocol` dependency to the generation of `smithy-build.json` in the `smithy4sUpdateLSPConfig` tasks of the codegen plugins (see [#1610](https://github.com/disneystreaming/smithy4s/pull/1610)).
14+
* Fix for the lenient union decoding [bug](https://github.com/disneystreaming/smithy4s/issues/1617) (see[#1620](https://github.com/disneystreaming/smithy4s/pull/1620)).
1415

1516
# 0.18.25
1617

modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala

+53-90
Original file line numberDiff line numberDiff line change
@@ -976,30 +976,63 @@ private[smithy4s] class SchemaVisitorJCodec(
976976

977977
private type Writer[A] = A => JsonWriter => Unit
978978

979-
private def taggedUnion[U](
980-
alternatives: Vector[Alt[U, _]]
981-
)(dispatch: Alt.Dispatcher[U]): JCodec[U] =
982-
new JCodec[U] {
983-
val expecting: String = "tagged-union"
979+
private abstract class TaggedUnionJCodec[U](alternatives: Vector[Alt[U, _]])(
980+
dispatch: Alt.Dispatcher[U]
981+
) extends JCodec[U] {
984982

985-
override def canBeKey: Boolean = false
983+
val expecting = "tagged-union"
986984

987-
def jsonLabel[A](alt: Alt[U, A]): String =
988-
alt.hints.get(JsonName) match {
989-
case None => alt.label
990-
case Some(x) => x.value
985+
override def canBeKey: Boolean = false
986+
987+
def jsonLabel[A](alt: Alt[U, A]): String =
988+
alt.hints.get(JsonName) match {
989+
case None => alt.label
990+
case Some(x) => x.value
991+
}
992+
993+
protected val handlerMap =
994+
new util.HashMap[String, (Cursor, JsonReader) => U] {
995+
def handler[A](alt: Alt[U, A]) = {
996+
val codec = apply(alt.schema)
997+
(cursor: Cursor, reader: JsonReader) =>
998+
alt.inject(cursor.decode(codec, reader))
991999
}
9921000

993-
private[this] val handlerMap =
994-
new util.HashMap[String, (Cursor, JsonReader) => U] {
995-
def handler[A](alt: Alt[U, A]) = {
996-
val codec = apply(alt.schema)
997-
(cursor: Cursor, reader: JsonReader) =>
998-
alt.inject(cursor.decode(codec, reader))
1001+
alternatives.foreach(alt => put(jsonLabel(alt), handler(alt)))
1002+
}
1003+
1004+
protected val precompiler = new smithy4s.schema.Alt.Precompiler[Writer] {
1005+
def apply[A](label: String, instance: Schema[A]): Writer[A] = {
1006+
val jsonLabel =
1007+
instance.hints.get(JsonName).map(_.value).getOrElse(label)
1008+
val jcodecA = instance.compile(self)
1009+
a =>
1010+
out => {
1011+
out.writeObjectStart()
1012+
out.writeKey(jsonLabel)
1013+
jcodecA.encodeValue(a, out)
1014+
out.writeObjectEnd()
9991015
}
1016+
}
1017+
}
1018+
protected val writer = dispatch.compile(precompiler)
10001019

1001-
alternatives.foreach(alt => put(jsonLabel(alt), handler(alt)))
1002-
}
1020+
def encodeValue(u: U, out: JsonWriter): Unit = {
1021+
writer(u)(out)
1022+
}
1023+
1024+
def decodeKey(in: JsonReader): U =
1025+
in.decodeError("Cannot use coproducts as keys")
1026+
1027+
def encodeKey(u: U, out: JsonWriter): Unit =
1028+
out.encodeError("Cannot use coproducts as keys")
1029+
1030+
}
1031+
1032+
private def taggedUnion[U](
1033+
alternatives: Vector[Alt[U, _]]
1034+
)(dispatch: Alt.Dispatcher[U]): JCodec[U] =
1035+
new TaggedUnionJCodec[U](alternatives)(dispatch) {
10031036

10041037
def decodeValue(cursor: Cursor, in: JsonReader): U =
10051038
if (in.isNextToken('{')) {
@@ -1020,66 +1053,20 @@ private[smithy4s] class SchemaVisitorJCodec(
10201053
}
10211054
}
10221055
} else in.decodeError("Expected JSON object")
1023-
1024-
val precompiler = new smithy4s.schema.Alt.Precompiler[Writer] {
1025-
def apply[A](label: String, instance: Schema[A]): Writer[A] = {
1026-
val jsonLabel =
1027-
instance.hints.get(JsonName).map(_.value).getOrElse(label)
1028-
val jcodecA = instance.compile(self)
1029-
a =>
1030-
out => {
1031-
out.writeObjectStart()
1032-
out.writeKey(jsonLabel)
1033-
jcodecA.encodeValue(a, out)
1034-
out.writeObjectEnd()
1035-
}
1036-
}
1037-
}
1038-
val writer = dispatch.compile(precompiler)
1039-
1040-
def encodeValue(u: U, out: JsonWriter): Unit = {
1041-
writer(u)(out)
1042-
}
1043-
1044-
def decodeKey(in: JsonReader): U =
1045-
in.decodeError("Cannot use coproducts as keys")
1046-
1047-
def encodeKey(u: U, out: JsonWriter): Unit =
1048-
out.encodeError("Cannot use coproducts as keys")
10491056
}
10501057

10511058
private def lenientTaggedUnion[U](
10521059
alternatives: Vector[Alt[U, _]]
10531060
)(dispatch: Alt.Dispatcher[U]): JCodec[U] =
1054-
new JCodec[U] {
1055-
val expecting: String = "tagged-union"
1056-
1057-
override def canBeKey: Boolean = false
1058-
1059-
def jsonLabel[A](alt: Alt[U, A]): String =
1060-
alt.hints.get(JsonName) match {
1061-
case None => alt.label
1062-
case Some(x) => x.value
1063-
}
1064-
1065-
private[this] val handlerMap =
1066-
new util.HashMap[String, (Cursor, JsonReader) => U] {
1067-
def handler[A](alt: Alt[U, A]) = {
1068-
val codec = apply(alt.schema)
1069-
(cursor: Cursor, reader: JsonReader) =>
1070-
alt.inject(cursor.decode(codec, reader))
1071-
}
1072-
1073-
alternatives.foreach(alt => put(jsonLabel(alt), handler(alt)))
1074-
}
1075-
1061+
new TaggedUnionJCodec[U](alternatives)(dispatch) {
10761062
def decodeValue(cursor: Cursor, in: JsonReader): U = {
10771063
var result: U = null.asInstanceOf[U]
10781064
if (in.isNextToken('{')) {
10791065
if (!in.isNextToken('}')) {
10801066
in.rollbackToken()
10811067
while ({
10821068
val key = in.readKeyAsString()
1069+
cursor.push(key)
10831070
val handler = handlerMap.get(key)
10841071
if (handler eq null) in.skip()
10851072
else if (in.isNextToken('n')) {
@@ -1103,31 +1090,7 @@ private[smithy4s] class SchemaVisitorJCodec(
11031090
}
11041091
} else in.decodeError("Expected JSON object")
11051092
}
1106-
val precompiler = new smithy4s.schema.Alt.Precompiler[Writer] {
1107-
def apply[A](label: String, instance: Schema[A]): Writer[A] = {
1108-
val jsonLabel =
1109-
instance.hints.get(JsonName).map(_.value).getOrElse(label)
1110-
val jcodecA = instance.compile(self)
1111-
a =>
1112-
out => {
1113-
out.writeObjectStart()
1114-
out.writeKey(jsonLabel)
1115-
jcodecA.encodeValue(a, out)
1116-
out.writeObjectEnd()
1117-
}
1118-
}
1119-
}
1120-
val writer = dispatch.compile(precompiler)
1121-
1122-
def encodeValue(u: U, out: JsonWriter): Unit = {
1123-
writer(u)(out)
1124-
}
1125-
1126-
def decodeKey(in: JsonReader): U =
1127-
in.decodeError("Cannot use coproducts as keys")
11281093

1129-
def encodeKey(u: U, out: JsonWriter): Unit =
1130-
out.encodeError("Cannot use coproducts as keys")
11311094
}
11321095

11331096
private def untaggedUnion[U](

modules/json/test/src/smithy4s/json/SchemaVisitorJCodecTests.scala

+38
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,44 @@ class SchemaVisitorJCodecTests() extends FunSuite {
406406
expect.same(readFromString[Either[Int, String]](json2), Left(1))
407407
}
408408

409+
test("Lenient and regular unions have the same error messages") {
410+
val json = """|{
411+
| "left" : {"foo": "b"}
412+
|}
413+
|""".stripMargin
414+
415+
val schema = Schema.either(
416+
Schema
417+
.struct[String](
418+
Schema.string
419+
.required[String]("bar", identity)
420+
)(identity),
421+
Schema
422+
.struct[String](
423+
Schema.string
424+
.required[String]("baz", identity)
425+
)(identity)
426+
)
427+
428+
val regularCodec =
429+
JsoniterCodecCompilerImpl.defaultJsoniterCodecCompiler.fromSchema(schema)
430+
val lenientCodec =
431+
JsoniterCodecCompilerImpl.defaultJsoniterCodecCompiler.withLenientTaggedUnionDecoding
432+
.fromSchema(schema)
433+
434+
def decodeCheck(codec: JsonCodec[Either[String, String]]) =
435+
expect.same(
436+
Try(
437+
readFromString[Either[String, String]](json)(codec)
438+
).toEither.left.map(_.getMessage),
439+
Left("Missing required field (path: .left.bar)")
440+
)
441+
442+
decodeCheck(regularCodec)
443+
decodeCheck(lenientCodec)
444+
445+
}
446+
409447
test("Untagged union are encoded / decoded") {
410448
val oneJ = """ {"three":"three_value"}"""
411449
val twoJ = """ {"four":4}"""

0 commit comments

Comments
 (0)