Type safety with Iron
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! 🙌