Add schema-iron module for Iron refinement type support#1097
Add schema-iron module for Iron refinement type support#1097EstebanMarin wants to merge 2 commits intozio:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new schema-iron module that integrates Iron refinement types with ZIO Blocks Schema, enabling automatic validation of refined types during schema derivation and decoding.
Changes:
- Adds new
schema-ironJVM-only module with Iron 2.6.0 dependency - Implements
ironSchemainline given instance that derivesSchema[A :| C]fromSchema[A]usingtransformOrFailwith Iron's runtime validation - Includes basic tests and README documentation
- Integrates module into build configuration and test commands
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
build.sbt |
Adds schema-iron module configuration with Iron dependency, test dependencies, and coverage thresholds; updates testJVM command |
schema-iron/src/main/scala/zio/blocks/schema/iron/package.scala |
Implements core integration via ironSchema inline given that validates Iron constraints during decode and unwraps during encode |
schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala |
Adds basic tests that verify schema derivation (but don't test actual validation behavior) |
schema-iron/README.md |
Provides usage examples, installation instructions, and documents known encoding limitations with opaque types |
Comments suppressed due to low confidence (7)
schema-iron/README.md:39
- This PR introduces a new public API (the
ironSchemagiven instance) but does not include corresponding documentation updates in thedocs/directory. According to project guidelines, new public APIs should have documentation underdocs/reference/.
Consider adding:
- A reference page at
docs/reference/iron-refinement-types.mdexplaining how to use Iron with ZIO Blocks Schema - An entry in the documentation navigation structure
- A mention in
docs/index.mdif appropriate (though this may be optional for integration modules)
At minimum, the reference documentation should include:
- How to set up the integration (imports, dependencies)
- Usage examples with common Iron constraints
- Explanation of how validation errors are reported
- The known limitations with opaque types and encoding
# ZIO Blocks Schema Iron
Integration between ZIO Blocks Schema and [Iron](https://github.com/Iltotore/iron) for type-safe refinement types.
## Installation
```scala
libraryDependencies += "dev.zio" %% "zio-blocks-schema-iron" % "0.0.1"
Usage
import zio.blocks.schema.*
import zio.blocks.schema.iron.given
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.*
case class Person(name: String, age: Int :| Positive)
object Person:
given Schema[Person] = Schema.derived
// Now you can use Person with any format
val jsonCodec = Schema[Person].derive(JsonFormat)
// Decoding with validation
val validJson = """{"name":"Alice","age":25}"""
val invalidJson = """{"name":"Bob","age":-5}"""
jsonCodec.decode(validJson.getBytes) // Right(Person(Alice,25))
jsonCodec.decode(invalidJson.getBytes) // Left(SchemaError: Should be strictly positive at: .age)The integration automatically derives Schema[A :| C] from Schema[A] with runtime validation using Iron's constraints.
Known Limitations
Due to how ZIO Blocks handles opaque types in derived schemas, encoding refined types may not work correctly in all cases. Decoding and validation work as expected.
**schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala:28**
* There's an extra blank line at the end of the file. The codebase typically has a single blank line at the end of Scala files.
}
**schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala:8**
* Test specs in this codebase consistently use `object` rather than `class` when extending `SchemaBaseSpec`. For consistency, this should be:
```scala
object IronSchemaSpec extends SchemaBaseSpec {
instead of:
class IronSchemaSpec extends SchemaBaseSpec {This pattern is used throughout the codebase in all other schema test specs (e.g., BsonCodecPrimitivesSpec, AvroFormatSpec, MessagePackFormatSpec, etc.).
class IronSchemaSpec extends SchemaBaseSpec {
schema-iron/src/test/scala/zio/blocks/schema/iron/IronSchemaSpec.scala:26
- The PR description claims encoding may not work correctly with opaque types, resulting in default values (e.g.,
0forInt). However, there are no tests that verify this behavior or document the limitation.
Recommended: Add tests that either:
- Verify encoding DOES work (if the issue has been resolved or doesn't occur in practice)
- Document the encoding limitation with an ignored/pending test that shows the actual vs. expected behavior
This would help future maintainers understand:
- Whether the issue actually exists
- What specific scenarios trigger the problem
- Whether it's been fixed in the future
Example structure:
test("encoding refined types preserves values") {
val person = Person("Alice", 10.refineUnsafe[Positive])
val codec = Schema[Person].derive(JsonFormat)
val encoded = codec.encodeToString(person)
assertTrue(encoded.contains("\"age\":10")) // Not "\"age\":0"
}.ignore // Remove .ignore if this passes def spec = suite("IronSchemaSpec")(
test("derive schema for refined types") {
val schema = summon[Schema[Person]]
assertTrue(schema != null)
},
test("refined type schema has correct structure") {
val ageSchema = summon[Schema[Int :| Positive]]
assertTrue(ageSchema != null)
}
)
schema-iron/src/main/scala/zio/blocks/schema/iron/package.scala:14
- There are extra blank lines at the end of the file. The codebase typically has a single blank line at the end of Scala files.
schema-iron/README.md:31 - The example comment showing the decode result is slightly inaccurate. The result would be
Person("Alice",25)(with quotes around the string "Alice"), notPerson(Alice,25)as shown. While this is a minor formatting issue in a comment, it's better to show the accurate toString representation.
jsonCodec.decode(validJson.getBytes) // Right(Person(Alice,25))
schema-iron/README.md:8
- The version "0.0.1" in the installation instructions is likely a placeholder. This should be updated to match the actual project version or use a placeholder like "@Version@" that can be replaced during documentation generation (as used in docs/index.md line 79).
libraryDependencies += "dev.zio" %% "zio-blocks-schema-iron" % "0.0.1"
| inline given ironSchema[A, C](using baseSchema: Schema[A], constraint: Constraint[A, C]): Schema[A :| C] = | ||
| baseSchema.transformOrFail( | ||
| a => a.refineEither[C].left.map(SchemaError.validationFailed), | ||
| refined => refined.asInstanceOf[A] | ||
| ) |
There was a problem hiding this comment.
This public API declaration is missing required Scaladoc documentation. According to project guidelines, all new public APIs must have comprehensive Scaladoc including:
- A clear one-line summary explaining what this given instance does
@tparamtags for type parametersAandCexplaining their purpose- A usage example demonstrating how to use this integration with Iron constraints
@returntag (if applicable for given instances)
The Scaladoc should explain that this automatically derives a Schema for Iron-refined types that validates the constraint during decoding and unwraps during encoding.
Example structure:
/** Automatically derives a Schema for Iron-refined types.
*
* This given instance enables automatic schema derivation for types refined with Iron constraints.
* During decoding, the constraint is validated and failure results in a SchemaError. During
* encoding, the refined value is unwrapped to its base type.
*
* @tparam A the base type being refined
* @tparam C the Iron constraint being applied
* @example
* {{{
* import io.github.iltotore.iron.*
* import io.github.iltotore.iron.constraint.numeric.*
* import zio.blocks.schema.*
* import zio.blocks.schema.iron.given
*
* case class Person(age: Int :| Positive)
* given Schema[Person] = Schema.derived
* }}}
*/
| def spec = suite("IronSchemaSpec")( | ||
| test("derive schema for refined types") { | ||
| val schema = summon[Schema[Person]] | ||
| assertTrue(schema != null) | ||
| }, | ||
|
|
||
| test("refined type schema has correct structure") { | ||
| val ageSchema = summon[Schema[Int :| Positive]] | ||
| assertTrue(ageSchema != null) | ||
| } | ||
| ) |
There was a problem hiding this comment.
These tests are inadequate and don't actually verify the core functionality of the Iron integration. The tests only check that schemas can be summoned (schema != null), but don't test:
- Validation on decode - The primary feature: that invalid values are rejected with appropriate error messages
- Valid values pass - That valid refined values decode successfully
- Round-trip behavior - That encoding and decoding work correctly
- Error messages - That validation failures produce clear, helpful error messages
The PR description claims "100% coverage" and states that "Decoding with validation works correctly", but these tests don't exercise any validation logic at all.
Recommended tests to add:
- Test decoding valid refined values (e.g., positive integers) succeeds
- Test decoding invalid values (e.g., negative integers for Positive constraint) fails with appropriate error
- Test round-trip encoding/decoding of valid refined values
- Test that error messages include the constraint violation details
You can use patterns from JsonTestUtils.roundTrip and JsonTestUtils.decodeError that are available via the test dependency on schema's test sources.
There was a problem hiding this comment.
I am testing a concept for now I agree test needs improvement
|
@EstebanMarin Please resolve conflicts, then good to merge! |
Add Iron Refinement Type Support
Implements
schema-ironmodule to enable Iron refinement types with ZIO Blocks schemas.What This Adds
scala
import zio.blocks.schema.iron.given
case class Person(name: String, age: Int :| Positive)
object Person:
given Schema[Person] = Schema.derived
// Automatic validation on decode
jsonCodec.decode("""{"age":25}""") // ✅ Right(Person(...))
jsonCodec.decode("""{"age":-5}""") // ❌ Left(SchemaError: Should be strictly positive)
Implementation
Derives
Schema[A :| C]fromSchema[A]usingtransformOrFailwith Iron's runtime validation.Known Issue: Opaque Types & Macro Derivation
Problem: Iron uses opaque type aliases (
type :| [A, C] = A). When ZIO Blocks' macro derives schemas for case classes with opaque-typed fields, it reads the field value incorrectly, resulting in default values (e.g.,0forInt) during encoding.Root Cause: The derived schema's field accessor doesn't properly handle opaque types at the macro expansion level.
Evidence:
scala
val person = Person("Alice", 10.refineUnsafe[Positive])
// person.age == 10 ✅
// encoded JSON: {"age":0} ❌
Testing
Created a local version of library and ran https://github.com/EstebanMarin/zio-sandbox/blob/main/src/main/scala/Main.scala