Automatically Generating Type-Safe Builders for Data Classes

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.

4 Likes

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

2 Likes

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.

1 Like

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.

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.

3 Likes

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

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.

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

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

I would love to see that as well. There is a library for auto-generation now: GitHub - juanchosaravia/autodsl: Auto-generate Kotlin DSL the only two dowsides it has it does not work with default values (imo acceptable), but less acceptable it does not give any compile-time safety for required values.

2 Likes

Adding compile-time safety for a builder is impossible without either writing a complex third party analysis tool or basically completely overhauling the entire language. You cannot use the type system, as the builder works by mutating its internal state, and thus does not change its own type. And while I don’t know exactly what is possible with contracts, I am pretty sure that this is well beyond that as well.

If you were to switch from Kotlin-style DSLs to Java style fluent interface, i.e. something that would by used like this:

builder.withPropertyA(valueA)
    .withPropertyB(valueB)
    ...

Then you could theoretically achieve type safety, however, it would require

  1. Each withX method would return a new object, copying all the values of the already assigned properties.
  2. Generating a class for every possible subset of properties required (one for the empty builder, one for the builder with only property a, one for the builder with only property b, one with both a and b etc.). The number of classes constructed would 2^n for n properties. And don’t even get me started on how awful the names would be, especially when auto generated.

In short: Not worth it.

3 Likes

Hi, I’m the creator of AutoDsl lib. Unfortunately, as Varia mentioned, it’s too complex to implement something like this, builders normally behaves in this way. Still I’m totally open for any suggestion/approach that I may not be aware of. If that’s the case please feel free to share it with me, send a PR or comment here:

Thanks!

1 Like

Does anyone have a working example based on Kapt? Seems like it should be possible, but has anyone tried it? Thanks

This is the first example I found when googling “kotlin kapt tutorial”. It looks decent, at least I didn’t see any obvious mistakes when I looked through it quickly:

If you want to see a real code example of kapt being used I suggest looking at moshi:

Also should also look into kotlin poet. It’s a library that can be used to easily generate kotlin code, handle imports and stuf

Last lib you might also want to look into is kotlinx.metadata. You will need this to read kotlins internal @Metadata annotation which contains information about kotlin constructs that java doesn’t know about, eg nullability, sealed/data classes, etc. Kapt is a wrapper around a java api so you don’t get that information for free. Square also has some easy to use wrapper for this. I think it’s part of kotlinpoet but I’m not 100% sure. It’s been a while since I last used any of this stuff.

1 Like