Skip to content

Conversation

@987Nabil
Copy link
Contributor

@987Nabil 987Nabil commented Jan 2, 2026

fixes #517
/claim #517

This PR is build up on my PR for #518 so #589 should be reviewed first.

@algora-pbc algora-pbc bot mentioned this pull request Jan 2, 2026
@987Nabil 987Nabil force-pushed the structural-schemas-517 branch from 7ee9866 to 13653d6 Compare January 3, 2026 08:06
@987Nabil 987Nabil requested review from Copilot, jdegoes and plokhotnyuk and removed request for jdegoes January 3, 2026 08:07
Copy link
Contributor

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.

deleted

@987Nabil
Copy link
Contributor Author

987Nabil commented Jan 3, 2026

@jdegoes @plokhotnyuk Structural types are a little tricky especially in Scala 3.
So for example

type PersonLike = { def name:String; def age:Int}

val aPersoon: PersonLike = new Selectable {
...
}

Will not be recognized by the compiler as a Selectable since the type Person is not a subtype of it.
so aPerson.name is a compile error, unless you import the implicit conversion that enables reflection.

One could suggest, to create a class

final class Record(...) extends Selectable {
...
}

So that this is returned by Schema#structural

val structuralSchema: Schema[ Record {def name: String; def age: Int}] = nominal.structural

But this is fundamentally flawed, since it defeats the purpose of the structural typing.
What I mean is, that if I have a

case class Person(name: String, age: Int)

This is not of type Record {def name: String; def age: Int} so structuralSchema would not be a schema for Person.
Maybe one could offer an implicit conversion, that uses a macro to compile time check that we can create a transformation from Person to Record {def name: String; def age: Int} but idk if this is really a good idea.

Instead my current solution offers that .structural does not return Record {def name: String; def age: Int} but {def name: String; def age: Int} and uses reflections. Which in return means, this is a JVM only solution.
Schema#structural on JS/Native leads to a compile errors.
Deriving a Schema for Record {def name: String; def age: Int} though works on all platforms

//Works on JVM, JS, Native
val structural = Schema.derived[Record {def name: String; def age: Int}]

@987Nabil 987Nabil force-pushed the structural-schemas-517 branch 6 times, most recently from 253ec74 to 1ebd60b Compare January 7, 2026 11:35
@jdegoes
Copy link
Member

jdegoes commented Jan 10, 2026

@987Nabil Please update, rebase, and refactor to handle refinement types on DynamicValue. 🙏

@jdegoes
Copy link
Member

jdegoes commented Jan 23, 2026

@987Nabil Pleaes re-open when ready. 🙏

@jdegoes jdegoes closed this Jan 23, 2026
@BenraouaneSoufiane
Copy link

@987Nabil Pleaes re-open when ready. 🙏

What to do with this, I was waiting him to fix 28 test errors, I'm going to fix them then submit my pr as they coming from this implementation not my implementation but my implementation depends on them!

@987Nabil 987Nabil reopened this Jan 25, 2026
@987Nabil 987Nabil force-pushed the structural-schemas-517 branch 3 times, most recently from 8b0f838 to eded7a2 Compare January 26, 2026 06:26
Copy link
Contributor

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

Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.


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


val typeName = normalizeTypeName(fieldInfos.toList.map { case (name, reflect) =>
(name, reflect.typeName.name)
(name, reflect.typeName.name)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Duplicate line detected. Line 220 is identical to line 219, creating a duplicate entry in the tuple list. This will cause the same field name and type name to be added twice to the fieldInfos list, leading to incorrect type name normalization.

Suggested change
(name, reflect.typeName.name)

Copilot uses AI. Check for mistakes.
987Nabil added a commit that referenced this pull request Jan 27, 2026
- Fix duplicate line bug in Scala 2 ToStructuralVersionSpecific.scala
- Add DynamicValue roundtrip tests to PureStructuralTypeSpec
- Remove TypeNameNormalizationSpec per review feedback
987Nabil added a commit that referenced this pull request Jan 27, 2026
- Delete EnumToUnionSpec.scala (tests enum derivation, not .structural)
- Delete SealedTraitToUnionSpec.scala (tests sealed trait derivation, not .structural)
- Delete AsPrimitiveSpec.scala (tests As/Into primitives, unrelated to structural types)
- Revert JsonSpec.scala additions (coverage tests unrelated to structural types)
- Fix weak assertions in EnumStructuralSpec.scala and UnionTypesSpec.scala to use exact Tag pattern matches
987Nabil added a commit that referenced this pull request Jan 27, 2026
Per reviewer feedback on PR #614:
- EnumToUnionSpec now tests schema.structural conversion for Scala 3 enums
- SealedTraitToUnionSpec now tests schema.structural for sealed traits
- Tests verify structural schemas are Variants with Tag markers
- Tests verify DynamicValue round-trip through structural schema
- Removed AsPrimitiveSpec (unrelated to structural types)
987Nabil added a commit that referenced this pull request Jan 27, 2026
- Fix duplicate line bug in Scala 2 ToStructuralVersionSpecific.scala
- Add DynamicValue roundtrip tests to PureStructuralTypeSpec
- Remove TypeNameNormalizationSpec per review feedback
@987Nabil 987Nabil force-pushed the structural-schemas-517 branch from 20d4776 to df780f4 Compare January 27, 2026 20:47
987Nabil added a commit that referenced this pull request Jan 28, 2026
- Delete EnumToUnionSpec.scala (tests enum derivation, not .structural)
- Delete SealedTraitToUnionSpec.scala (tests sealed trait derivation, not .structural)
- Delete AsPrimitiveSpec.scala (tests As/Into primitives, unrelated to structural types)
- Revert JsonSpec.scala additions (coverage tests unrelated to structural types)
- Fix weak assertions in EnumStructuralSpec.scala and UnionTypesSpec.scala to use exact Tag pattern matches
987Nabil added a commit that referenced this pull request Jan 28, 2026
Per reviewer feedback on PR #614:
- EnumToUnionSpec now tests schema.structural conversion for Scala 3 enums
- SealedTraitToUnionSpec now tests schema.structural for sealed traits
- Tests verify structural schemas are Variants with Tag markers
- Tests verify DynamicValue round-trip through structural schema
- Removed AsPrimitiveSpec (unrelated to structural types)
987Nabil added a commit that referenced this pull request Jan 28, 2026
- Fix duplicate line bug in Scala 2 ToStructuralVersionSpecific.scala
- Add DynamicValue roundtrip tests to PureStructuralTypeSpec
- Remove TypeNameNormalizationSpec per review feedback
@987Nabil 987Nabil force-pushed the structural-schemas-517 branch from e1b1ee3 to b2b5fd0 Compare January 28, 2026 16:47
Implement ToStructural type class for converting nominal types to pure
structural types on JVM. Key features:

- Schema.structural extension method via SchemaVersionSpecific trait
- Fully unpacks opaque types (Scala 3) and ZIO Prelude newtypes recursively
- Handles nested type constructors (e.g., List[UserId] -> List[String])
- JVM-only via Platform.supportsReflection checks
- Uses pure Selectable types with selectDynamic (no custom apply/map)

Also fixes Array/IArray/ArraySeq constructors to implement emptyObject
and use Math.max(sizeHint, 1) for proper initialization.
- Fix fullyUnpackType to use isOpaqueAlias for proper opaque type detection
- Add OpaqueTypeUnpackingSpec for testing opaque type handling in structural types
- Add enum structural conversion tests to EnumToUnionSpec
- Move enum structural conversion tests from shared to JVM-only (EnumStructuralSpec)
- Remove structural tests from EnumToUnionSpec (shared) to fix JS/Native compilation
- Add TupleStructuralSpec for tuple structural conversion tests
- Add StructuralRoundTripSpec for product/sum type round-trip and structure tests
- Restore StructuralTypeCompileErrorSpec (JS/Native) to original error expectations
- Restore StructuralTypeSpec (Scala 2) to original form
- Add StructuralBindingSpec to exercise structural schema encoding/decoding paths
- Add AsPrimitiveSpec with tests for As.reverseInto, As.reverse, As.apply,
  Into.identity, and all numeric widening/narrowing conversions
- Extend JsonSpec with additional coverage for:
  - PrimitiveValue conversions (all date/time types, Currency, UUID)
  - Number comparison edge cases (invalid number strings)
  - Parse error handling (invalid JSON, unclosed structures)
  - ByteBuffer encoding/decoding
  - CharSequence parsing
  - Object/Array varargs constructors
  - sortKeys/dropNulls/dropEmpty on non-objects
  - asUnsafe method

Achieves branch coverage 83.31% (>= 83% required)
- Delete EnumToUnionSpec.scala (tests enum derivation, not .structural)
- Delete SealedTraitToUnionSpec.scala (tests sealed trait derivation, not .structural)
- Delete AsPrimitiveSpec.scala (tests As/Into primitives, unrelated to structural types)
- Revert JsonSpec.scala additions (coverage tests unrelated to structural types)
- Fix weak assertions in EnumStructuralSpec.scala and UnionTypesSpec.scala to use exact Tag pattern matches
Per reviewer feedback on PR #614:
- EnumToUnionSpec now tests schema.structural conversion for Scala 3 enums
- SealedTraitToUnionSpec now tests schema.structural for sealed traits
- Tests verify structural schemas are Variants with Tag markers
- Tests verify DynamicValue round-trip through structural schema
- Removed AsPrimitiveSpec (unrelated to structural types)
Replace contains() assertions with exact equality checks for structural
type names per reviewer feedback.
- Changed structural types from type members to method members (def Tag: "Red")
- Added ReflectiveDeconstructor for reading fields via reflection
- Added ReflectiveVariantDiscriminator for Tag-based case discrimination
- Added ReflectiveMatcher for variant case matching
- Sorted variant cases alphabetically for consistency
- Updated tests to use anonymous structural instances per JDG review
- Fix duplicate line bug in Scala 2 ToStructuralVersionSpecific.scala
- Add DynamicValue roundtrip tests to PureStructuralTypeSpec
- Remove TypeNameNormalizationSpec per review feedback
- Rename UnsupportedTypeErrorSpec to SupportedTypesSpec (moved to scala-3/)
- Add explicit Schema[{def ...}] type annotations to all typeCheck tests
- Fix DynamicValue.Record(Vector(...)) compilation errors after rebase
- Add typeCheck tests to enum, sealed trait, product, and tuple specs
- All tests pass on Scala 3.3.7 and 2.13.18
Replace all typeName.contains() and typeName == assertions with typeCheck
tests that verify structural type signatures compile correctly.

Changes:
- Remove typeName assertions from 13 test files
- Delete TypeNameNormalizationSpec.scala (tests TypeName going away)
- Preserve all runtime tests (roundtrips, Reflect structure checks)
- Add typeCheck tests where needed for coverage

This prepares for TypeName removal in a separate PR.
Change structural type encoding from flat Tag-based format to nested case
wrapper format:
- Old: {def Tag: "Circle"; def radius: Double}
- New: {def Circle: {def radius: Double}}

Key changes:
- Rename buildStructuralTypeWithTag to buildNestedCaseType
- Add ReflectiveVariantCaseDeconstructor for nested value extraction
- Update ReflectiveVariantDiscriminator to check case name method existence
- Update ReflectiveMatcher to invoke case name method for nested values
- Update normalizeUnionCaseTypeName for {CaseName:{fields}} format
- Update all structural test specs for new encoding
@987Nabil 987Nabil force-pushed the structural-schemas-517 branch from b2b5fd0 to e6c5e70 Compare January 28, 2026 17:04
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.

Add structural schemas

4 participants