This article is a follow up to Type safety with refined, make sure to check it out if you haven’t.

In this one we’ll repeat the same thing but this time using iron.

Unsafe approach

Similarly to the previous post, we’ll try to refactor the code below to eliminate the handwritten value checks. The goal is to express our business requirements using the type-system.

//> using scala "2"

case class UnsafeOrderLine(product: String, quantity: Int)
object UnsafeOrderLine {
  def safeApply(product: String, quantity: Int): UnsafeOrderLine = {
    if (product.isEmpty())
      throw new RuntimeException("Product is empty")
    else if (quantity <= 0)
      throw new RuntimeException("Quantity lower than 1")
    else
      UnsafeOrderLine(product, quantity)
  }

}

// Works fine!
println(UnsafeOrderLine.safeApply("123", 10))
// Throws runtime exception 👇
UnsafeOrderLine.safeApply("", 10)

Iron

Iron is a library for refined types developed in Scala 3. To use it in your project simply add the dependency to sbt/scala-cli

Go with iron if you only want the refinements:

//> using lib "io.github.iltotore::iron:2.2.1" 

You can add circe support that will prove useful later:

//> using lib "io.github.iltotore::iron-circe:2.2.1"

See a full list of additional modules in the documentation at https://iltotore.github.io/iron/docs/modules/index.html

Safe approach

To replicate our unsafe code with Iron, we can refactor to a one-liner:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*

case class OrderLine(product: String :| Not[Empty], quantity: Int :| Positive)

As you have noted iron uses :| to apply the Constraint to your initial type. Iron comes with a lot of built in constraints, check out the reference for full list of builtin constraints.

Let’s move on with the implementation and try modelling the Order that contains the lines and an id.

type UUIDConstraint = DescribedAs[Match["^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"], "Should be an UUID"]

case class Order(orderId: String :| UUIDConstraint, lines: NonEmptyList[OrderLine])

Although iron comes with a builtin UUID validation, the snippet showcases how easy it is to build your own constraint. See the docs for other examples.

Serialization

Now that we have used iron to model the domain, for a complete comparison to the refined post let’s see how to combine it with Circe.

First let’s add the missing dependencies. We are adding circe parser, codec generics.

//> using scala "3"
//> using lib "org.typelevel::cats-core:2.10.0"
//> using lib "io.github.iltotore::iron:2.2.1"
//> using lib "io.github.iltotore::iron-circe:2.2.1"
//> using lib "io.circe::circe-parser:0.14.5"
//> using lib "io.circe::circe-generic:0.14.5"

import cats.data.NonEmptyList

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import io.github.iltotore.iron.circe.given

import io.circe.Codec
import io.circe.syntax.*
import io.circe.parser.*

With all the imports in place, let’s add codecs to our data types:

case class OrderLine(product: String :| Not[Empty], quantity: Int :| Positive) derives Codec.AsObject

case class Order(orderId: String :| UUIDConstraint, lines: NonEmptyList[OrderLine]) derives Codec.AsObject

Noticed the difference? since iron is Scala 3 only, we can use derives Codec.AsObject to derive instead of using companion object and implicits for better readability.

Now let’s have a look at full snippet that attempts to serialize and deserialize some data.

//> using scala "3"
//> using lib "org.typelevel::cats-core:2.10.0"
//> using lib "io.github.iltotore::iron:2.2.1"
//> using lib "io.github.iltotore::iron-circe:2.2.1"
//> using lib "io.circe::circe-parser:0.14.5"
//> using lib "io.circe::circe-generic:0.14.5"

import cats.data.NonEmptyList

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*
import io.github.iltotore.iron.circe.given

import io.circe.Codec
import io.circe.syntax.*
import io.circe.parser.*

case class OrderLine(product: String :| Not[Empty], quantity: Int :| Positive) derives Codec.AsObject

type UUIDConstraint = DescribedAs[Match["^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"], "Should be an UUID"]

case class Order(orderId: String :| UUIDConstraint, lines: NonEmptyList[OrderLine]) derives Codec.AsObject

val orderLines = NonEmptyList.of(
  OrderLine("100", 10),
  OrderLine("101", 5)
)

val order = Order(
  "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  orderLines
)

println(order.asJson.toString)

val json = """
{
  "orderId" : "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
"""
println(decode[Order](json))


val invalidJson = """
{
  "orderId" : "not-an-uuid",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
"""
println(decode[Order](invalidJson))

When you run it, this is the output for our program

$ scala-cli main.sc
Compiling project (Scala 3.3.0, JVM)
Compiled project (Scala 3.3.0, JVM)
{
  "orderId" : "c93dd655-caca-4c99-aad8-3d91ad7a1b7e",
  "lines" : [
    {
      "product" : "100",
      "quantity" : 10
    },
    {
      "product" : "101",
      "quantity" : 5
    }
  ]
}
Right(Order(c93dd655-caca-4c99-aad8-3d91ad7a1b7e,NonEmptyList(OrderLine(100,10), OrderLine(101,5))))
Left(DecodingFailure at .orderId: Should be an UUID)

As you can see using iron is as simple as refined.

Refinements in Scala

Why another refined library? you might ask. There are obviously some differences between the libraries. Here’s my attempt to compare them:

Aspect Refined Iron
Basic predicates
User defined predicates
Compile-time only
Supported by tapir
Supported by circe
Supported by doobie
Scala 3 support ⚠️
Scala 2 support
Scala JS support
Scala Native support

⚠️ Macros are still not supported, see https://github.com/fthomas/refined/issues/932

Hope this helps you choose the right library for your project. Iron is definitely a younger project, which is reflected by limited 3rd party library support, but no runtime overhead might be the deal breaker.

Summary

The bottom line is that they are both fun to use and extremely useful. Kudos to the authors and contributors for all the hard work! 🙌