Skip to content

Commit 7fd45d2

Browse files
authored
Fix for DynamicValue.Record field order mixed up by serialization (#284)
* Reproducer for Dynamic Record field order issue * Fix
1 parent f3623b3 commit 7fd45d2

File tree

4 files changed

+40
-21
lines changed

4 files changed

+40
-21
lines changed

tests/shared/src/test/scala/zio/schema/DynamicValueSpec.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ object DynamicValueSpec extends ZIOSpecDefault {
7171
},
7272
test("round-trip semiDynamic") {
7373
val gen = for {
74-
schemaAndGen <- SchemaGen.anyGenericRecordAndGen
74+
schemaAndGen <- SchemaGen.anyGenericRecordAndGen()
7575
(schema, valueGen) = schemaAndGen
7676
value <- valueGen
7777
} yield schema -> value

tests/shared/src/test/scala/zio/schema/SchemaGen.scala

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import zio.test.{ Gen, Sized }
99

1010
object SchemaGen {
1111

12-
val anyLabel: Gen[Sized, String] = Gen.alphaNumericStringBounded(1, 3)
12+
val anyLabel: Gen[Sized, String] = Gen.alphaNumericStringBounded(1, 15)
1313

1414
def anyStructure(
1515
schemaGen: Gen[Sized, Schema[_]]
@@ -25,9 +25,9 @@ object SchemaGen {
2525
}
2626
}
2727

28-
def anyStructure[A](schema: Schema[A]): Gen[Sized, Seq[Schema.Field[A]]] =
28+
def anyStructure[A](schema: Schema[A], max: Int = 3): Gen[Sized, Seq[Schema.Field[A]]] =
2929
Gen
30-
.setOfBounded(1, 3)(
30+
.setOfBounded(1, max)(
3131
anyLabel.map(Schema.Field(_, schema))
3232
)
3333
.map(_.toSeq)
@@ -215,10 +215,10 @@ object SchemaGen {
215215

216216
type GenericRecordAndGen = (Schema[ListMap[String, _]], Gen[Sized, ListMap[String, _]])
217217

218-
val anyGenericRecordAndGen: Gen[Sized, GenericRecordAndGen] =
218+
def anyGenericRecordAndGen(maxFieldCount: Int = 3): Gen[Sized, GenericRecordAndGen] =
219219
for {
220220
(schema, gen) <- anyPrimitiveAndGen
221-
structure <- anyStructure(schema)
221+
structure <- anyStructure(schema, maxFieldCount)
222222
} yield {
223223
val valueGen = Gen
224224
.const(structure.map(_.label))
@@ -234,17 +234,17 @@ object SchemaGen {
234234

235235
type RecordAndValue = (Schema[ListMap[String, _]], ListMap[String, _])
236236

237-
val anyRecordAndValue: Gen[Sized, RecordAndValue] =
237+
def anyRecordAndValue(maxFieldCount: Int = 3): Gen[Sized, RecordAndValue] =
238238
for {
239-
(schema, gen) <- anyGenericRecordAndGen
239+
(schema, gen) <- anyGenericRecordAndGen(maxFieldCount)
240240
value <- gen
241241
} yield schema -> value
242242

243243
val anyRecordOfRecordsAndValue: Gen[Sized, RecordAndValue] =
244244
for {
245-
(schema1, gen1) <- anyGenericRecordAndGen
246-
(schema2, gen2) <- anyGenericRecordAndGen
247-
(schema3, gen3) <- anyGenericRecordAndGen
245+
(schema1, gen1) <- anyGenericRecordAndGen()
246+
(schema2, gen2) <- anyGenericRecordAndGen()
247+
(schema3, gen3) <- anyGenericRecordAndGen()
248248
keys <- Gen.setOfN(3)(anyLabel).map(_.toSeq)
249249
(key1, value1) <- Gen.const(keys(0)).zip(gen1)
250250
(key2, value2) <- Gen.const(keys(1)).zip(gen2)

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
288288
},
289289
test("of records") {
290290
check(for {
291-
(left, a) <- SchemaGen.anyRecordAndValue
291+
(left, a) <- SchemaGen.anyRecordAndValue()
292292
primitiveSchema <- SchemaGen.anyPrimitive
293293
} yield (Schema.EitherSchema(left, primitiveSchema), Left(a))) {
294294
case (schema, value) => assertEncodesThenDecodes(schema, value)
@@ -326,7 +326,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
326326
}
327327
},
328328
test("of record") {
329-
check(SchemaGen.anyRecordAndValue) {
329+
check(SchemaGen.anyRecordAndValue()) {
330330
case (schema, value) =>
331331
assertEncodesThenDecodes(Schema.Optional(schema), Some(value)) &>
332332
assertEncodesThenDecodes(Schema.Optional(schema), None)
@@ -442,12 +442,12 @@ object JsonCodecSpec extends ZIOSpecDefault {
442442
),
443443
suite("record")(
444444
test("any") {
445-
check(SchemaGen.anyRecordAndValue) {
445+
check(SchemaGen.anyRecordAndValue()) {
446446
case (schema, value) => assertEncodesThenDecodes(schema, value)
447447
}
448448
},
449449
test("minimal test case") {
450-
SchemaGen.anyRecordAndValue.runHead.flatMap {
450+
SchemaGen.anyRecordAndValue().runHead.flatMap {
451451
case Some((schema, value)) =>
452452
val key = new String(Array('\u0007', '\n'))
453453
val embedded = Schema.record(Schema.Field(key, schema))
@@ -462,7 +462,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
462462
}
463463
},
464464
test("of primitives") {
465-
check(SchemaGen.anyRecordAndValue) {
465+
check(SchemaGen.anyRecordAndValue()) {
466466
case (schema, value) => assertEncodesThenDecodes(schema, value)
467467
}
468468
},
@@ -616,7 +616,25 @@ object JsonCodecSpec extends ZIOSpecDefault {
616616
compare = compareRandomRecords
617617
)
618618
}
619-
}
619+
},
620+
test("deserialized dynamic record converted to typed value") {
621+
check(SchemaGen.anyRecordAndValue(maxFieldCount = 15)) {
622+
case (schema, value) =>
623+
val dyn = DynamicValue.fromSchemaAndValue(schema, value)
624+
ZStream
625+
.succeed(dyn)
626+
.via(JsonCodec.encoder(Schema.dynamicValue))
627+
.via(JsonCodec.decoder(Schema.dynamicValue))
628+
.map(_.toTypedValue(schema))
629+
.runHead
630+
.map { result =>
631+
val resultList = result.get.toOption.get.toList
632+
assertTrue(
633+
resultList == value.toList
634+
)
635+
}
636+
}
637+
} @@ TestAspect.sized(1000) @@ TestAspect.samples(1000)
620638
)
621639
)
622640

zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,10 +2071,11 @@ private[schema] object DynamicValueSchema { self =>
20712071
private val recordCase: Schema.Case[DynamicValue.Record, DynamicValue] =
20722072
Schema.Case(
20732073
"Record",
2074-
Schema.CaseClass1[Map[String, DynamicValue], DynamicValue.Record](
2075-
Schema.Field("values", Schema.defer(Schema.map(Schema.primitive[String], DynamicValueSchema()))),
2076-
map => DynamicValue.Record(ListMap(map.toSeq: _*)),
2077-
record => record.values
2074+
Schema.CaseClass1[Chunk[(String, DynamicValue)], DynamicValue.Record](
2075+
Schema
2076+
.Field("values", Schema.defer(Schema.chunk(Schema.tuple2(Schema.primitive[String], DynamicValueSchema())))),
2077+
chunk => DynamicValue.Record(ListMap(chunk.toSeq: _*)),
2078+
record => Chunk.fromIterable(record.values)
20782079
),
20792080
_.asInstanceOf[DynamicValue.Record]
20802081
)

0 commit comments

Comments
 (0)