Skip to content

Support constructor registry in DynamicValue.toTypedValue for AST-rebuilt schemas (#511) - #1137

Open
chengdazhi wants to merge 1 commit into
zio:mainfrom
chengdazhi:fix-511-typed-value-registry
Open

Support constructor registry in DynamicValue.toTypedValue for AST-rebuilt schemas (#511)#1137
chengdazhi wants to merge 1 commit into
zio:mainfrom
chengdazhi:fix-511-typed-value-registry

Conversation

@chengdazhi

Copy link
Copy Markdown

/claim 511

Closes #511

Problem

When a schema is rebuilt from its AST (schema.ast.toSchema), records are materialized as Schema.GenericRecord, which only knows how to build a ListMap[String, _]. Converting a dynamic record with such a schema therefore yields a raw ListMap instead of the original case class — the typed constructor was lost when the schema was turned into a MetaSchema.

Solution

Implements the registry approach suggested by @jdegoes in the issue discussion (#511 (comment)):

  • New overload DynamicValue.toTypedValue[A](registry: PartialFunction[TypeId, DynamicValue.Constructor[_]])(implicit schema: Schema[A]): Either[DecodeError, A].
  • New DynamicValue.Constructor[A], which builds an A from the decoded field values of a record, with Constructor.fromSchema and Constructor.registry helpers to build a registry directly from record schemas (including the implicit derived ones, so no reflection is involved).
  • The registry is threaded through the whole conversion, so records are reconstructed at any nesting level: nested records, Option, sequences, sets, maps, Either, tuples and enum cases — not just at the top level (this is where the naive attempt in dynamic value to typed using a registry #725 stops).
  • Records missing from the registry keep the existing behaviour and decode to their generic representation (ListMap[String, _]), so the change is fully backwards compatible; decodeStructure retains its original signature.

Usage with the reproduction from the issue:

final case class User(name: String, age: Int)
implicit val schema: Schema[User] = DeriveSchema.gen[User]

val user        = User("John", 30)
val userDynamic = schema.toDynamic(user)
val registry    = DynamicValue.Constructor.registry(schema)

userDynamic.toTypedValue(registry)(schema.ast.toSchema)
// Right(User("John", 30))

Tests

  • 5 new tests in DynamicValueSpec covering: top-level records rebuilt from their AST, fallback to generic records when no constructor is registered, nested records / Option / Chunk, enum cases, and failure when the dynamic record does not match the registered constructor.
  • Verified locally: zioSchemaJVM/test, zioSchemaDerivationJVM/test, testsJVM/test on Scala 2.12.21, 2.13.18 and 3.3.8; zioSchemaJS/test, testsJS/testOnly zio.schema.DynamicValueSpec on 2.13.18; zioSchemaNative/compile, testsNative/compile; scalafmtCheckAll, scalafix --check on the touched modules; zioSchemaJVM/mimaReportBinaryIssues (no problems).

Note: this PR was prepared with the assistance of an AI coding agent; the changes were reviewed and verified by running the checks listed above.

…uilt schemas (zio#511)

Schemas materialized from a MetaSchema (schema.ast.toSchema) represent
records as Schema.GenericRecord and lose the information needed to
construct the original typed representation, so converting a dynamic
record with such a schema yields a raw ListMap instead of the case
class.

Add an overload of toTypedValue that takes a constructor registry
(PartialFunction[TypeId, DynamicValue.Constructor[_]]), as suggested by
@jdegoes in the issue discussion. The registry is threaded through the
whole conversion, so records are reconstructed at any nesting level:
nested records, options, sequences, maps, eithers, tuples and enum
cases. Records missing from the registry keep the existing generic
record (ListMap) behaviour.

Also add DynamicValue.Constructor with helpers to build a registry
directly from record schemas.
@chengdazhi
chengdazhi requested a review from a team as a code owner July 13, 2026 08:30
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to convert to TypedValue

2 participants