mapstruct-kotlin icon indicating copy to clipboard operation
mapstruct-kotlin copied to clipboard

Allow Builders to leverage Kotlin defaults, or kotlin builder notation when required fields are missing

Open natashashams opened this issue 4 years ago • 4 comments

ex data class in kotlin -

import com.github.pozo.KotlinBuilder

@KotlinBuilder
data class ContactInfo(val phoneNumbers: List<PhoneNumber> = emptyList(),
                       val emails: List<EmailAddress> = emptyList(),
                       val addresses: List<PostalAddress> = emptyList()
)

The create function within the generated builder looks like:

public ContactInfo create() { return new ContactInfo(phoneNumbers, emails, addresses); }

The create here uses an all args constructor. However, if the object builders never set a value for one mandatory field (addresses field, for example) the create() call using an all args constructor will try to set addresses to null, which will throw an NPE type error since this value can't be null in the target kotlin object.

I would like a way for the create() method to:

  • leverage the default values in Kotlin that an object may have (usually non-nullable types have this), setting a field to the default value if a default exists

This would be amazing and very helpful to generate more accurate builders!!

natashashams avatar Apr 10 '20 05:04 natashashams

Thank you for reporting this issue. I'll look into it.

Pozo avatar Apr 10 '20 22:04 Pozo

@natashashams I tried to reproduce the issue what you described without any luck. It seems the issue description is not that clear to me.

Could you please create a very basic project where you can reproduce the described null pointer ? You can use the debug module as a template https://github.com/Pozo/mapstruct-kotlin/tree/master/debug

Pozo avatar Apr 13 '20 21:04 Pozo

@Pozo I might be able to help clarify as I'm looking for this feature as well.

If a data class specifies a default value for a field in the constructor, the default is not propagated to the builder and will always be invoked with null if the builder never set the field.

Passing null to a class constructor field is different than passing no value for that field in Kotlin.

For example

data class Foo(
    val bar: String = "bar"
)
import java.lang.String;

public final class FooBuilder {
  private String bar;

  public FooBuilder setBar(String bar) {
    this.bar = bar;
    return this;
  }

  public Foo create() {
    return new Foo(bar);
  }

  public static FooBuilder builder() {
    return new FooBuilder();
  }
}

If no value is passed to the builder, it can result in a NullPointerException because bar is non-nullable or the field would unexpectedly be set to null in the nullable case.

val foo = FooBuilder
   .builder()
   .create()
// Created with 'new Foo(null)'

mtraynham avatar Apr 14 '20 17:04 mtraynham

I also came across this problem and tried to fix it. However it seems not that easy to find the fields default values. The only way I found was accessing the java source code as described on stackoverflow article 6373145. However in the java sources the default values are "hidden" in a special constructor which is generated by Kotlin. It would be nice to access the original Kotlin code, but this seems to be even more cumbersome, see here.

As a workaround, to at least generate usable mappers, you can just specify your defaults again in the mapper definition. So for the example above you should create a mapper like this:

@Mapper
abstract class FooMapper {
    @Mappings(value = [
        Mapping(target = "bar", source = "bar")
    ])
    abstract fun map(bar: String = "bar"): Foo
}

m-kay avatar May 04 '20 05:05 m-kay