vert-x3 / vertx-lang-kotlin

Vert.x for Kotlin
Apache License 2.0
296 stars 68 forks source link

Improve @DataObject compatibility with Kotlin Data Classes #198

Open rgmz opened 3 years ago

rgmz commented 3 years ago

Description

Presently, there are a few issues which make Kotlin Data Classes incompatible/difficult to use with Vert.x's @DataObject code generation (e.g. #17, #43).

For example, instead of using data classes idiomatically:

@DataObject
data class Author(val name: String, val email: String)

you're forced to write code which is significantly lengthier and less 'safe': can be instantiated without all parameters, parameters are defined with mutable var instead of val, etc.:

@DataObject
class Author() {
  lateinit var name: String
  lateinit var email: String

  constructor(json: JsonObject) : this() {
    this.name = json.getString("name", "")
    this.email = json.getString("email", "")
  }

  fun toJson(): JsonObject {
    return JsonObject.mapFrom(this)
  }
}

From what I recall, the two main sources of ire are:

  1. Required JsonObject constructor

    Data object classes must provide a constructor which takes a single io.vertx.core.json.JsonObject or java.lang.String parameter

    Unlike Java, [secondary constructors in Kotlin must call the primary constructor](https ://kotlinlang.org/docs/classes.html#secondary-constructors) :

    @DataObject
    data class Author(val name: String, val email: String) {
        // Illegal: name and email must be passed to this(). 
        constructor(json: JsonObject): this() {
            AuthorConverter.fromJson(json, this)
        }
    
        // Legal, but difficult to maintain if your class has several parameters.
        constructor(json: JsonObject): this(
            name = json.getString("name"),
            email = json.getString("email")
        )
    }
  2. Required no-arg constructor (for the generated converter and ...Of methods)

As mentioned above, data classes must be instantiated with all the required parameters. That constraint makes them incompatible with the generated Converter.fromJson methods, as that requires the class to be instantiated, and the properties to be mutible:

  public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, YourType obj) {
   for (java.util.Map.Entry<String, Object> member : json) {
      switch (member.getKey()) {
        case "aBoolean":
          if (member.getValue() instanceof Boolean) {
            obj.setABoolean((Boolean)member.getValue());
          }
          break;
    ...

It also makes them incompatible with the generated yourTypeOf(...) methods, which similarly requires the class to be instantiated before it can populate values: https://github.com/vert-x3/vertx-lang-kotlin/blob/ae327c43acdd6cb40d0d606b46cedd39f646d352/vertx-lang-kotlin/src/main/kotlin/io/vertx/kotlin/pgclient/PgConnectOptions.kt#L148-L152

Solutions

With everything I previously mentioned in mind, what can we do to improve the experience?

Some ideas off the top of my head:

  1. @ConstructorBinding Spring Boot introduced the @ConstructorBinding annotation, which eliminates the need for a no-arg constructor :

    @ConstructorBinding
    @ConfigurationProperties("blog")
    data class BlogProperties(var title: String, val banner: Banner) {
        data class Banner(val title: String? = null, val content: String)
    }

    I'm not sure the feasibility of implementing something like this, as it's presumably dynamic and could lead to security issues. Neverthless, I'm including it here for inspiration.

  2. Static @JsonCreator method In Jackson, you can define a static method annotated with @JsonCreator, which will be used to instantiate the class:

    data class UserId(private val value: String) {
        companion object {
            @JvmStatic
            @JsonCreator
            fun create(value: String) = UserId(value.toLowerCase())
       }
    }

    This could be an alternative to the JsonObject constructor, and mitigate having to stuff values into the this() block.

  3. Kotlinx.Serialization Kotlinx.Serialization is a native serialization library which generates encodes and decoders at compile time.

    Perhaps we could leverage kotlinx.serialization to generate safe encoders and decoders?

  4. Custom Codegen We could augment vertx-lang-kotlin-gen, or write a Kotlin Compiler Plugin, to generate Kotlin-friendly converters and ...Of methods.

vietj commented 3 years ago

did you look at https://vertx.io/docs/vertx-core/kotlin/#_data_objects_builders ?

rgmz commented 3 years ago

Yes, that's what I was referring to by ...Of methods.

Those are useful for constructing Java data objects in Kotlin but, in my experience, aren't as compelling or reliable for generating Kotlin data objects.

I've opened two issues relating to the codegen of these builders (#158,

161), and there are probably other issues rooting from Kotlin's

idiosyncrasies. That said, fixing those wouldn't address the larger pain points, like builders requiring a no-arg constructor.

Fundamentally, the issue is that DataObject constraints (and utilities) aren't compatible with Kotlin's data classes, which are a core language feature. That's a huge pain point, and I'd like to see how/if we can improve the experience.

P.S., Are there plans to support Java Records? If so, those would face similar challenges.


On Mon., May 31, 2021, 9:21 a.m. Julien Viet, @.***> wrote:

did you look at https://vertx.io/docs/vertx-core/kotlin/#_data_objects_builders ?

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/vert-x3/vertx-lang-kotlin/issues/198#issuecomment-851487927, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHVFC7RWQT4JQMPMTSAWIBTTQOEMDANCNFSM4523Y5EA .