Skip to content

Add schema-iron module for Iron refinement type support#1097

Open
EstebanMarin wants to merge 2 commits intozio:mainfrom
EstebanMarin:refined-types-integration
Open

Add schema-iron module for Iron refinement type support#1097
EstebanMarin wants to merge 2 commits intozio:mainfrom
EstebanMarin:refined-types-integration

Conversation

@EstebanMarin
Copy link
Contributor

@EstebanMarin EstebanMarin commented Feb 18, 2026

Add Iron Refinement Type Support

Implements schema-iron module 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] from Schema[A] using transformOrFail with 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., 0 for Int) 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

Copilot AI review requested due to automatic review settings February 18, 2026 22:50
@EstebanMarin EstebanMarin mentioned this pull request Feb 18, 2026
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

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-iron JVM-only module with Iron 2.6.0 dependency
  • Implements ironSchema inline given instance that derives Schema[A :| C] from Schema[A] using transformOrFail with 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 ironSchema given instance) but does not include corresponding documentation updates in the docs/ directory. According to project guidelines, new public APIs should have documentation under docs/reference/.

Consider adding:

  1. A reference page at docs/reference/iron-refinement-types.md explaining how to use Iron with ZIO Blocks Schema
  2. An entry in the documentation navigation structure
  3. A mention in docs/index.md if 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., 0 for Int). However, there are no tests that verify this behavior or document the limitation.

Recommended: Add tests that either:

  1. Verify encoding DOES work (if the issue has been resolved or doesn't occur in practice)
  2. 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"), not Person(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"

Comment on lines +7 to +11
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]
)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

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
  • @tparam tags for type parameters A and C explaining their purpose
  • A usage example demonstrating how to use this integration with Iron constraints
  • @return tag (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
  * }}}
  */

Copilot generated this review using guidance from repository custom instructions.
Comment on lines +16 to +26
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)
}
)
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

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:

  1. Validation on decode - The primary feature: that invalid values are rejected with appropriate error messages
  2. Valid values pass - That valid refined values decode successfully
  3. Round-trip behavior - That encoding and decoding work correctly
  4. 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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am testing a concept for now I agree test needs improvement

@jdegoes
Copy link
Member

jdegoes commented Mar 9, 2026

@EstebanMarin Please resolve conflicts, then good to merge!

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.

3 participants