Automatically Generating Type-Safe Builders for Data Classes


#1

Making type-safe builders for data classes is very tedious.

Builders for data classes that use mutable variables are easy enough but when assign-once (read-only) variables are used this requires even more tedious work (e.g. by calling copy for each build step or by pratically duplicating the data class so that there is one with some assign-once variables and another with only mutable variables and using the latter as an intermediary class to collect the information to be passed to the initial data class’s constructor).

Automatically generating low-overhead builders for data classes would be, I think, a agreat addition to Kotlin.


#2

Bumping this topic since I think it’s still worth exploring. OP said it best: in many situations you are creating temporary variables to assign things to, only to re-use them in building the data object. Why not allow it directly? Here are some suggestions for syntax:

data class Person(val name: String, val age: Int)

Option 1

Person.new { // or 'of', 'create', 'build', etc.
  name = "John"
  age = 23
}

This method would be added at compile time, just like copy, and it would expect every non-optional variable be assigned within the block. If any were missing, a compile error would be thrown.

Option 2

Person.new {
   override val name = "John"
   override val age = 23
}

Perhaps this is a more idiomatic and Kotlin-esque way to do it. This is essentially a constructor block, so properties can be overriden as necessary. For this to work, an abstract instance of the data class needs to exist, with all non-optional fields being declared abstract and all with defaults being considered open


#3

I would love to see a automatically generated builder for data classes, but this is a feature which can be easily be implemented using annotation processing so I am not sure if this should necessarily be part of the language itself. Adding a Builder to each data class would double the number of classes required to use data classes which can lead to problems on Android I think.

I think adding an Annotation (@CreateBuilder or something) would just be as effective without the overhead of having Builders for classes which don’t need any. That way you could even possibly add a validation function if you want @CreateBuilder(::validate), which can than be called at runtime.


#4

Are you saying this is already possible for someone to create using some type of annotation? I’m not sure how that would work. I know Groovy has AST modifiers, which sounds about like what would be necessary for that.


#5

Yes, I am pretty sure using Kapt you should be able to do something like:

  1. find all classes with annotation @CreateBuilder
  2. using kotlin metadata you should even be able to check whether or not they are data classes
  3. generate a new class in the same package with the same fields, but add also a setter for all fields

Accessing the Kotlin metadata is quite complex but you might not even have to do it. I guess this would work even better if you’d use the kotlin compiler plugin but there is no official public API for it yet, so I would not advice using it (it is used internally though e.g. for serialization and they said they want to publish it at some point).
Kapt is quite powerful. It gives you access to the Java RoundEnvironment which gives you basically the same features reflection would, only at compile time. That way you can generate additional source files which than will be compiled with the rest of your program. You can not however edit or change existing code.


#6

Thank you, I’ll look into this. I still think that it should exist as a language feature, however.


#7

I would personally thing having it as a official extension like the coroutine or serialization library would make more sense for the reasons I explained above but in general I wouldn’t mind either way.


#8

Per a Slack discussion, this is a similar way of achieving this behavior right now:

Person(
  name = run { /* complex logic */ }
  age = when { /* complex logic /* }
)