Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve @DataObject compatibility with Kotlin Data Classes #198

Open
rgmz opened this issue May 31, 2021 · 2 comments
Open

Improve @DataObject compatibility with Kotlin Data Classes #198

rgmz opened this issue May 31, 2021 · 2 comments

Comments

@rgmz
Copy link
Contributor

rgmz commented May 31, 2021

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:

user: String? = null): PgConnectOptions = io.vertx.pgclient.PgConnectOptions().apply {
if (applicationLayerProtocols != null) {
this.setApplicationLayerProtocols(applicationLayerProtocols.toList())
}

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
Copy link
Contributor

vietj commented May 31, 2021

@rgmz
Copy link
Contributor Author

rgmz commented May 31, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

2 participants