How Scala Macros Simplify Protobuf‑Java ↔ Scala Case Class Conversions

This article explains a Scala‑based DSL that uses macros and implicit parameters to generate type‑safe, boiler‑plate‑free conversion code between protobuf‑java messages and Scala case classes, showing examples, builder customisation, and upcoming Scala 3 support.

GrowingIO Tech Team
GrowingIO Tech Team
GrowingIO Tech Team
How Scala Macros Simplify Protobuf‑Java ↔ Scala Case Class Conversions

Background

In GrowingIO backend development we use gRPC for microservice communication, defining Protobuf messages for each service and generating Java code with protoc. Scala services use case classes, so converting between protobuf‑java objects and Scala case classes becomes repetitive and error‑prone, especially with nested types.

Typical mappings (e.g., java.util.ListSeq, TimestampZonedDateTime, Option[String]StringValue) exist, but a generic, type‑safe solution that minimizes boilerplate was needed.

Inspired by Play‑JSON Reader/Writer and Chimney macro‑based conversion, we created scala-protobuf-java, a DSL that combines Scala macros with implicit‑parameter design.

Solution Effect

Defining a case class User and a protobuf message UserPB, manual conversion requires many lines of code. With the DSL a single call Protoable[User,UserPB].toProto(user) performs the conversion safely and concisely.

DSL Design

The core traits are Protoable[-S,+P] and Scalable[+S,-P], representing conversion directions.

trait Protoable[-S, +P] {
  def toProto(entity: S): P
}
trait Scalable[+S, -P] {
  def toScala(proto: P): S
}

Companion objects provide implicit converters for primitive and common types, such as Java boxed types, StringValue, and TimestampZonedDateTime.

Macro‑Generated Code

Using the DSL we write concrete converters for User and UserPB. The macro expands the boilerplate, handling optional fields, collections, and nested types automatically.

new Protoable[User, UserPB] {
  override def toProto(entity: User): UserPB = {
    val builder = UserPB.newBuilder()
    builder.setId(entity.id)
    builder.setName(entity.name)
    if (entity.phoneNumber.isDefined) {
      builder.setPhoneNumber(implicitly[Protoable[String,StringValue]].toProto(entity.phoneNumber))
    }
    builder.addAllHobbies(implicitly[Protoable[Seq[String], java.util.List[String]]].toProto(entity.hobbies))
    builder.build
  }
}

new Scalable[User, UserPB] {
  override def toScala(proto: UserPB): User = {
    User(
      id = proto.getId,
      name = proto.getName,
      phoneNumber = if (proto.hasPhoneNumber) Some(implicitly[Scalable[String,StringValue]].toScala(proto.getPhoneNumber)) else None,
      hobbies = implicitly[Scalable[Seq[String], java.util.List[String]]].toScala(proto.getHobbiesList)
    )
  }
}

Builder Customisation

For field‑level rules we provide ScalableBuilder and ProtoableBuilder that let users inject lambdas, e.g., ensuring an id is non‑negative.

val scalable = ScalableBuilder[User,UserPB]
  .setField(_.id, pb => if (pb.getId < 0) 0 else pb.getId)
  .build
scalable.toScala(...)

Scala 3 Support

Scala 3 introduces cleaner implicit definitions and a new inline/Quotes macro system, which can simplify the DSL further, although some issues remain, such as protoc‑generated Java files not compiling.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DSLBackend DevelopmentProtobufmacroScala
GrowingIO Tech Team
Written by

GrowingIO Tech Team

The official technical account of GrowingIO, showcasing our tech innovations, experience summaries, and cutting‑edge black‑tech.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.