Skip to content

Improve DynamicValue error handling#1041

Open
Godzilla675 wants to merge 3 commits intozio:mainfrom
Godzilla675:issue-480-accumulating-validation
Open

Improve DynamicValue error handling#1041
Godzilla675 wants to merge 3 commits intozio:mainfrom
Godzilla675:issue-480-accumulating-validation

Conversation

@Godzilla675
Copy link
Copy Markdown

Fixes #480

Summary

  • add DynamicValue.toTypedValueAccumulating as a backward-compatible accumulating API
  • accumulate decode errors across records, tuples, sequences, sets, and dictionary entries
  • add focused DynamicValueSpec coverage for success-path parity and multi-error accumulation

Validation

  • testsJVM/testOnly zio.schema.DynamicValueSpec
  • zioSchemaJVM/scalafmtCheck
  • testsJVM/scalafmtCheck

/claim #480

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Godzilla675 Godzilla675 requested a review from a team as a code owner March 12, 2026 22:41
Copilot AI review requested due to automatic review settings March 12, 2026 22:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an accumulating decode API for DynamicValue that can collect multiple decode errors (rather than failing fast) and extends test coverage to validate success-path parity and multi-error accumulation.

Changes:

  • Introduce DynamicValue.toTypedValueAccumulating backed by an accumulating Validation-based decoder.
  • Accumulate decode errors across tuples, sequences, sets, maps, and record structures.
  • Add DynamicValueSpec tests for parity on successful decodes and for multi-error accumulation scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
zio-schema/shared/src/main/scala/zio/schema/DynamicValue.scala Adds accumulating decode API and record-structure accumulating decoder.
tests/shared/src/test/scala/zio/schema/DynamicValueSpec.scala Adds focused tests for accumulating behavior and success-path parity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +315 to +331
def decodeStructureAccumulating(
values: ListMap[String, DynamicValue],
structure: Chunk[Schema.Field[_, _]]
): Validation[DecodeError, ListMap[String, _]] = {
val keys = values.keySet
keys.foldLeft[Validation[DecodeError, ListMap[String, Any]]](Validation.succeed(ListMap.empty)) {
case (acc, key) =>
(structure.find(_.name == key), values.get(key)) match {
case (Some(field), Some(value)) =>
acc.zipWithPar(
value.toTypedValueAccumulatingLazyError(field.schema)
)((record, value) => record + (key -> value))
case _ =>
acc.zipWithPar(Validation.fail(DecodeError.IncompatibleShape(values, structure)))((record, _) => record)
}
}
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeStructureAccumulating folds over values.keySet, which can (a) miss schema fields that are absent from values (so missing-field / incompatible-shape errors won’t be accumulated), and (b) produce a record map in an order unrelated to structure. This is particularly problematic for Schema.Record[A] decoding, where s.construct(Chunk.fromIterable(m.values)) is typically positional and expects schema-field ordering. A concrete fix is to drive decoding primarily from structure (preserving field order and detecting missing values), and separately detect/accumulate errors for unexpected extra keys in values.

Copilot uses AI. Check for mistakes.
Comment on lines +77 to +82
.foldLeft[Validation[DecodeError, Chunk[t]]](Validation.succeed(Chunk.empty)) { (acc, value) =>
acc.zipWithPar(
value.toTypedValueAccumulatingLazyError(schema.elementSchema)
)(_ :+ _)
}
.map(schema.fromChunk)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chunk appends via (_ :+ _) can lead to quadratic behavior for large sequences because repeated appends may copy increasingly large chunks. Prefer accumulating into a builder/collection with efficient append (or accumulate in reverse with prepend-then-reverse) and convert once at the end, while still using Validation to accumulate errors.

Suggested change
.foldLeft[Validation[DecodeError, Chunk[t]]](Validation.succeed(Chunk.empty)) { (acc, value) =>
acc.zipWithPar(
value.toTypedValueAccumulatingLazyError(schema.elementSchema)
)(_ :+ _)
}
.map(schema.fromChunk)
.foldLeft[Validation[DecodeError, List[t]]](Validation.succeed(List.empty[t])) { (acc, value) =>
acc.zipWithPar(
value.toTypedValueAccumulatingLazyError(schema.elementSchema)
)((list, elem) => elem :: list)
}
.map { reversedList =>
schema.fromChunk(Chunk.fromIterable(reversedList.reverse))
}

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +120
result match {
case ZValidation.Success(_, value) => assertTrue(value == person)
case ZValidation.Failure(_, _) => assertTrue(false)
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using assertTrue(false) on the failure branch discards useful failure information and makes debugging harder. Prefer asserting directly on the Validation shape/value using ZIO Test assertions (e.g., assert that it is Success with the expected value), so failures report the collected errors.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +89
case (DynamicValue.SetValue(values), schema: Schema.Set[t]) =>
values.foldLeft[Validation[DecodeError, Set[t]]](Validation.succeed(Set.empty[t])) { (acc, value) =>
acc.zipWithPar(
value.toTypedValueAccumulatingLazyError(schema.elementSchema)
)(_ + _)
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds new accumulating behavior for SetValue and Dictionary element decoding, but the new DynamicValueSpec cases only cover record/tuple/sequence accumulation. Add tests that demonstrate multiple independent failures are accumulated for sets and maps as well (e.g., multiple bad keys/values and multiple bad set elements), to validate the newly introduced branches.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +114
case (DynamicValue.Dictionary(entries), schema: Schema.Map[k, v]) =>
entries.foldLeft[Validation[DecodeError, Map[k, v]]](Validation.succeed(Map.empty[k, v])) { (acc, entry) =>
val typedKey = entry._1.toTypedValueAccumulatingLazyError(schema.keySchema)
val typedValue = entry._2.toTypedValueAccumulatingLazyError(schema.valueSchema)
acc.zipWithPar(
typedKey.zipWithPar(typedValue)(_ -> _)
)(_ + _)
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds new accumulating behavior for SetValue and Dictionary element decoding, but the new DynamicValueSpec cases only cover record/tuple/sequence accumulation. Add tests that demonstrate multiple independent failures are accumulated for sets and maps as well (e.g., multiple bad keys/values and multiple bad set elements), to validate the newly introduced branches.

Copilot uses AI. Check for mistakes.
Godzilla675 and others added 2 commits March 13, 2026 00:52
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve DynamicValue error handling

2 participants