Feature Request: Constructor parameter default value when receiving null input

Hi there. I am in the process of converting a large Java code base to Kotlin, and I’ve run into an issue that makes this transition less than smooth. I’d like to suggest a small change to how class constructors work, particularly for data classes.

As an example, I have a Java class like this:

class MyClass {
  String name;
  List<String> addresses;

  public MyClass(String name, List<String> addresses) {
    this.name = name == null ? "" : name
    this.addresses = addresses == null ? Collections.emptyList() : addresses
  }

The idea here being that when a constructor parameter is null, we substitute a non-null default.

Kotlin can’t quite do this without either a deferred init, lazyinit, secondary constructor, or a companion object overriding the invoke operator. And with data classes, the first two options don’t appear possible.

data class MyClass(
  val name:String? = "",
  val addresses: List<String>? = emptyList()
)

In this case, I will only get my default value if the parameter is not specified at all, but if the input to the parameter is null, then the field becomes null. If I make the fields non-nullable, then existing Java code that calls this constructor hits NullPointerException where it didn’t before. (For example, a serialization library such as Jackson or Kryo can cause breakages, particularly in an environment where different versions of running code have to be able to talk to each other.)

It would be really convenient to have some way to apply the default value to a constructor field WHEN the input is null, and not just when it’s not provided. Something like:

data class MyClass(
  val name: String ?: "",
  val addresses: List<String> ?: emptyList()
)

The intent here is that the field inside the class is non-nullable (String, not String?), but the parameter can accept null and assign a default to the field.

Also, I know that I can use @JvmOverloads to create versions of the constructor with varying parameters, but (a) this doesn’t address the issue since existing calls use the full constructor anyway, and (b) our code base has data classes with a dozen or more fields, so creating permutations of the constructor is not in any way practical.

TL;DR: Please add a syntax to constructor field declarations that allows the constructor to assign a default value to a field when the value provided for the parameter is null. This will simplify the creation of data classes without requiring workarounds like overriding the invoke operator.

Thanks!

3 Likes

From my perspective, not allowing nulls in the first place would resolve your all issues:

data class MyClass(val name: String, val addresses: List<String>)

but if you absolutely want to allow nulls as possible input you could for example do something like this:

data class MyClass private constructor(val name: String, val addresses: List<String>) {
	companion object {
		operator fun invoke(name: String?, addresses: List<String>?) = MyClass(
			name = name ?: "",
			addresses = addresses ?: emptyList()
		)
	}
}
2 Likes

That’s one of the things I was hoping to avoid having to do. Wouldn’t it be much easier to just say “If null then value” right in the constructor? When I have a lot of constructor parameters, that’s a lot of extra code to have to write to do something that should be really simple.

1 Like

Polluting your own model for the sake of possible errors in external input is never a good idea. It should be either the client’s responsibility to provide a valid input or there should be some kind of middle layer to validate said input.

6 Likes

I also would like to clarify that this is to support interop with Java code (Java calling into Kotlin), where I don’t necessarily control the Java side of the code. We have a contract to not break existing code, so when I convert to Kotlin, I need to keep the public interfaces the same as much as is possible, but I would also like to take advantage of Kotlin’s internal null handling where Java made it a lot harder.

1 Like

This seems like it would be best implemented as a compiler plugin or an annotation processor. I can quickly rig up a compiler plugin doing this if you want. The issue with it being a compiler plugin is that the ability to pass in nulls won’t be visible to Kotlin code and it’ll only be visible to Java code, though.

I really don’t understand how this would pollute anything in the model. Wouldn’t it make the model a lot cleaner? I can’t just say “You can’t send me null now” without forcing a client to update their code, which we can’t just force on them on a whim. (More to the point, we have hundreds of tests that use this code, so forcing us to update all of those tests just to convert one class is infeasible in the short term.)

Basically, we’re in a situation where we need to preserve some existing behavior, and the current workarounds for that preservation are pretty convoluted and, dare I say, hacky. This feels to me like a very simple thing that would make Kotlin much more elegant for this use-case and significantly reduce barrier-to-entry.

2 Likes

I’m not sure how that would help - as I mentioned, we depend on libraries like Jackson and Kryo in our project, and we have to preserve compatibility with older versions of the code that haven’t been upgraded to work with the newly updated code. So I don’t know if a compiler plugin would help here since, for example, an instance of this class may be created by the Kryo deserializer in a way that might not benefit from that plugin.

1 Like

A compiler plugin would simply make sure that there’s a constructor accepting null for all the default parameters that then sets the default value for them automatically

Is it possible to do that in a way that doesn’t cause compile-time conflicts?

Are you talking about an auto-generated syntax that would look something like this?

data class MyClass(val name = String, val addresses = List<String>) {
  constructor(name: String?, addresses: List<String>?) : this(name = name ?: "", addresses = addresses ?: emptyList())
}

I don’t believe this would be possible since the two constructors have the same outward signature and the only difference between them is that one accepts nulls while the other doesn’t.

1 Like

To be clear, I can do all of this in a reasonably non-hacky way using a regular class, but I would very much like to use data class since it has extra niceties like the copy and equals methods that I otherwise have to write by hand.

1 Like

Not even that per se because you can realistically make a compiler plugin that changes the nullability of the primary constructor and then auto-magically performs the null-to-default-value conversions, while making it absolutely hidden away from the user. It’ll be in the same vain that @JvmOverloads works in for example since it does change a lot about how the function works, but it’s still fine at the end

That could work, then. Would it be a per-parameter annotation so I can specify what the default is? Or would it use the existing default (“not specified”) and be a class-level annotation?

Also, would it allow for other parameters to actually be null?

data class MyClass(
  val name: String = "",
  val addresses: List<String> = emptyList(),
  val optionalConfig: Configuration? = null
)

If the annotation can differentiate between “I want to accept null and substitute non-null” and “This field can actually be null”, then that will probably work.

1 Like

Simply, it would be applied to each constructor (or more widely just any function) (also perhaps to a class to apply to all of its constructors for convenience’s sake) and it’d work by iterating over each parameter, and if it has a default value, then make that parameter nullable and add a line in the constructor to assign the value of that parameter to be the default value (by copying it) if it is null. Tbh I don’t know what to really do when a parameter is clearly nullable but has a default value. I guess either do nothing and optionally give a warning or just treat it like you still want the parameter to have the default value when it isn’t null.

This is the line of thinking I was going down when searching for a way to accomplish what I needed to do here. Would I want a constructor-level or class-level annotation to specify this behavior? What if I want it to apply to just one field? Would I need it to be a per-field annotation? What would I call it? etc.

This is why I believe a language construct to do this would be far more elegant. Sure, we can use proxy classes to do this as well, but in our case that wouldn’t work because, again, we’d need to be able to do this in a way that doesn’t break compatibility with older clients and interop.

1 Like

@KieferSkunk A compiler plugin would simply work just like a language construct (if you squint hard enough). It can be configured to be applied on both parameter-level, class-level, and also function-level, just depending on where you place the annotation.

In fact, I decided to take on the challenge for those past 7 hours, and so I’ve done it! I made a compiler plugin that does exactly what you requested. Take a look at the git repo and specifically at the sample project, but the quick TL;DR of it is that with the annotation @NullDefaults, you can annotate your classes (for constructors), your functions/constructors, or specific default parameters, and the plugin picks up on that and modifies the constructor/function to account for the null case. Here’s the sample example itself; kotlin code:

data class Configuration @JvmOverloads constructor(@Nullable @NullDefaults val data: String = "42")

@NullDefaults
data class MyClass @JvmOverloads constructor(
  // The @Nullable makes sure that Java doesn't see false warnings when passing null to this. At the same time, Kotlin
  // absolutely ignores it and still views the parameters as Not-Null
  @Nullable val name: String = "",
  @Nullable val addresses: List<String> = emptyList(),
  val optionalConfig: Configuration? = null
)

@JvmOverloads
fun makeNetworkCall(
  rootUrl: String = "example.com",
  @Nullable @NullDefaults addressCount: Int? = 2,
  @Nullable @NullDefaults requestUrl: String = createUrlFrom2Parts(rootUrl, addressCount.toString())
): List<String> {
  println(rootUrl)
  println(addressCount)
  println(requestUrl)
  return listOf("Hello", "World")
}

@JvmOverloads
@NullDefaults
fun createUrlFrom2Parts(@Nullable firstPart: String = "foo", @Nullable secondPart: String = "bar") = "$firstPart/$secondPart"

fun main() {
  Main.main(null)
}

And then the java:

public class Main {
    public static void main(String[] args) {
        MyClass myClass = new MyClass(null, null, null);
        MyClass myClass2 = new MyClass(null, MyClassKt.makeNetworkCall("hello.world.org", null, null));
        MyClass myClass3 = new MyClass("test", null, new Configuration(MyClassKt.createUrlFrom2Parts(null, "bazzz")));
        Configuration defaultConfiguration = new Configuration(null);
        System.out.println(myClass);
        System.out.println(myClass2);
        System.out.println(myClass3);
        System.out.println(defaultConfiguration);
    }
}

and, in short, it does exactly what you expect it to; it views those null arguments as a sign to use the default values, and so it substitutes those in and continues on with its normal functionality.

4 Likes

That’s pretty cool. :slight_smile: Thanks! I do still think a language construct (two characters, the way I suggested it at least) would be a lot more elegant, but this would do as a substitute.

Does the @Nullable annotation HAVE to be specified on every non-nullable parameter when we do this?

Any chance you can add Maven support for it and get it up on Maven Central? I could very much use that right away.

1 Like

Oh it doesn’t have to be added in the slightest. I just personally hate warnings, lol! But you can of course suppress them. I tried to do some complex stuff with like JSR 305 and other stuff like that but it didn’t work. Also, I haven’t quite tested this, but I suspect that, if you have the Kotlin code in its own separate module, that the @Nullable annotations won’t be needed at all since the actual .class files do have those fields as nullable, but it’s just that Intellij tries to be smart and figure out nullability and other rules quickly if you’re in the same module. But yes, the @Nullable annotation is absolutely not needed. I can also add you a setting to just specify class names in the gradle config as a quick and dirty way to not even require the @NullDefaults annotation.

It’s already available on Jitpack because, last time I checked, Maven Central requires me to have my own website, but I’ll dig into that deeper right now. For now, however, basically follow what the same app is using in its settings.gradle.kts and build.gradle.kts. To simplify that further, actually, you really just need to do a few simple steps. Firstly, go to your settings.gradle.kts and add this at the top:

pluginManagement {
  repositories {
    gradlePluginPortal()
    maven(url = "https://jitpack.io")
  }
}

(This step won’t be needed when the plugin is up on the Gradle Plugin Portal, which it should be in a few days hopefully).

Then in your build.gradle.kts, add this to your plugins block:

plugins {
    ...
    id("io.github.kyay10.kotlin-null-defaults") version "0.1.0"
}

and add this to your repositories:

repositories {
    mavenCentral()
    maven(url = "https://jitpack.io")
    ... any other repos that you might be using
}

And that should literally be it. The plugin automatically adds the @NullDefaults annotation on your classpath, and so after a gradle sync you should see it show up in auto-complete. The plugin also technically works for multiplatform, even though the use-cases for other platforms are probably much less useful. Also, the copy method also works with the same null-to-default-value fallback mechanism, and so if for some reason you had Java code relying on that, then you should be easily able to get it working.

Don’t hesitate to notify me if you find any bugs or have any feedback, and meanwhile I’ll go down the mavenCentral rabbit hole!

Unfortunately, we aren’t using Gradle. Our project is built on Maven, and that’s not something I can easily change. So it would be helpful to be able to pull it as a Maven dependency.

1 Like

Is adding jitpack.io as a maven repo a no-go? I’m currently looking into Maven central, but AFAIK just adding a repository should work, right? IIRC, something like this should just work:

	<repositories>
		<repository>
		    <id>jitpack.io</id>
		    <url>https://jitpack.io</url>
		</repository>
	</repositories>
    ...
            <plugin>
                <artifactId>kotlin-maven-plugin</artifactId>
                <groupId>org.jetbrains.kotlin</groupId>
                <version>${kotlin.version}</version>
                ...
                <configuration>
                    <compilerPlugins>
                        <plugin>kotlin-null-defaults</plugin>
                    </compilerPlugins>
                </configuration>

                <dependencies>
	            <dependency>
	                <groupId>io.github.kyay10.kotlin-null-defaults</groupId>
	                <artifactId>kotlin-plugin</artifactId>
	                <version>0.1.0</version>
	            </dependency>
                </dependencies>
            </plugin>