Feature Request: Constructor parameter default value when receiving null input

Scratch that, I finally got it to publish to maven central. You should be able to apply it with the <compilerPlugins> configuration now! just keep in mind that the package is now io.github.kyay10 because SonaType insisted on that lol.

Okay, tried that, but it turns out that we’re using our own internal repository manager, and I’m not actually sure where it syncs its packages from. Currently can’t download your plugin from there, but I was able to get it from the jitpack repo.

On build, the maven-kotlin-plugin complains that the “kotlin-null-defaults” plugin doesn’t exist.

[ERROR] Failed to execute goal org.jetbrains.kotlin:kotlin-maven-plugin:1.5.21:compile (compile) on project data-classes: Plugin not found: kotlin-null-defaults: java.util.NoSuchElementException
[ERROR]       role: org.jetbrains.kotlin.maven.KotlinMavenPluginExtension
[ERROR]   roleHint: kotlin-null-defaults

Relevant portions of my pom (omitted other stuff):

    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url> <!-- A JetBrains maven/gradle plugin repository -->
        </repository>
    </repositories>

    <properties>
        <kotlin.compiler.nullplugin.version>0.1.1</kotlin.compiler.nullplugin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Special plugin from JetBrains to enable default values on null parameters for JVM interop -->
            <dependency>
                <groupId>io.github.kyay10.kotlin-null-defaults</groupId>
                <artifactId>kotlin-plugin</artifactId>
                <version>${kotlin.compiler.nullplugin.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <!-- This section specifies common dependencies for all child modules. -->
    <dependencies>
        <dependency>
            <groupId>io.github.kyay10.kotlin-null-defaults</groupId>
            <artifactId>kotlin-plugin</artifactId>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
                                <sourceDir>${project.basedir}/src/main/java</sourceDir>
                            </sourceDirs>
                        </configuration>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <goals> <goal>test-compile</goal> </goals>
                        <configuration>
                            <sourceDirs>
                                <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
                                <sourceDir>${project.basedir}/src/test/java</sourceDir>
                            </sourceDirs>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <compilerPlugins>
                        <plugin>kotlin-null-defaults</plugin>
                    </compilerPlugins>
                </configuration>
            </plugin>
        </pluginManagement>
    </build>

UPDATE: Our local repo manager just cloned your plugin from Maven Central, so now I can access it from there. :slight_smile: I had written this message several hours ago, but the site restricted the number of replies I could make in 24 hours. :stuck_out_tongue:

1 Like

Your internal one might be syncing from Maven Central every once in a while I guess. You can easily check that it’s on Maven Central by adding maven central in your <repositories> I guess. Jitpack does work, too, and so yeah it hopefully shouldn’t be an issue. Do try the plugin though and tell me if you run into any issues

Edit: Oh good I just saw your update. I think the site restriction only happens when you’re a new/inactive user, and so hopefully that limitation will be removed from you really soon.

Sorry, I am having trouble with the plugin - namely, the kotlin-maven-plugin says it doesn’t recognize it. Could you check my pom fragments above and tell me if I’m missing something?

1 Like

Maybe the dependecyManagement block could be messing it up?

I doubt it. I can see the annotation in the IDE, and the maven builder isn’t complaining that the artifact doesn’t exist. It’s specifically when it begins the compile step that it runs into this issue. If I change the version number or the name of the artifact in the dependencyManagement section, then it says it’s unable to download the artifact.

I followed your directions above to add the “configuration” section with the compiler plugin into the “kotlin-maven-plugin”. My suspicion is that the name of the plugin it wants to use is incorrect.

1 Like

Wait, it sees the annotation? As in, it sees the annotation without you specifically adding the prelude artifact as a dependency? Because if so, then the plugin must be working (I think)

I can see and use the annotation within IDEA using the updates to the pom. I cannot build my project from the maven command-line - mvn clean install

BTW, in case I get rate-limited again, is there another way I can go back and forth with you on this? I can’t seem to send you a private message, so email or a Discord server or something?

1 Like

To be more specific, I can put the annotation into my code without error. However, I can’t verify whether it builds the code correctly at the moment in IDEA since I have other reasons why the project won’t finish compiling (I’m still working on the code itself). But I should be able to get far enough along in the build process with the CLI that I can see the compile-time errors in my code (e.g. unresolved symbols), but it’s halting with that error before I can get that far.

1 Like

Not addressing your main question, but you really should be using the Elvis operator

    this.name = name ?: ""
    this.addresses = addresses ?: emptyList()
1 Like

The part that you quoted is the Java example, to demonstrate the behavior I’d like to replicate in the primary data constructor.

1 Like

Just a quick update: Been working with kyay10 on testing the plugin. We’re almost there - it works with the Maven CLI now but isn’t working for me in IDEA. Should hopefully be in a fully working state soon.

1 Like

Why not leave in peace classes used in a public interface and make an adapter which would convert nullable DTOs to your internal kotlinish types? Just like FooRequest and BarResponse in a REST API.

1 Like

As I mentioned, this is a library we’re working on here, and we have an obligation to maintain existing API contracts as much as possible. As such, introducing new “fronting” classes like this would not be acceptable, and due to a quirk of our system, moving the actual DTO to a different class and turning the existing one into a shallow converter is also not an option. (Not to mention, very un-Kotlin - that makes sense for MVC-style systems, and we use that model for MVC, but this library isn’t part of that.)

I do still think it’s an artificial limitation of this language to force someone to miss out on all the niceties of a data class just because they need to be able to accept a null input from existing Java code and turn it into a non-null field, without also forcing them to store a copy of the input. Constructors that intercept and substitute null values are very common, and not just in Java, but that use case precludes the Kotlin data class, when a very simple syntax would take care of it easily.

Also, I’ll point out that the Elvis operator is actively encouraged just about everywhere else in Kotlin to handle this exact “if null” case. Why would it be “polluting” a data model to use it in a constructor specifically to avoid having to store and later handle a null value?

All that said, I found a way to accomplish what I needed without this feature and without the plugin that kyay10 was so nice to write. I encourage him to continue development and testing of that plugin, as I’m sure it will help others.

1 Like

I can’t believe how long this discussion is dragged already :slight_smile: The whole idea of data and value classes is to store data as it is, without any additional “under the table” conversion logic. Even if said logic includes handling possible null values. If you need to implement it anyway then it already doesn’t qualify as data classes anymore and you should just do as you please using normal classes.

If Kotlin example is not good enough then take a look at Scala or even Lombok. There you have exactly the same situation with constructors generated from @Data/@Value annotations, but until now nobody changed their bahaviour by adding some fallback logic on null values.

Are you really telling me that if I want to handle a simple null case for Java interop, I shouldn’t use Kotlin? Don’t you think that sounds a bit extreme?

And doesn’t it seem just a little silly to you that asking to handle a single parameter in this way requires the dev to then write all of their own logic for methods like toString() and equals(), when that would just be a clone of what the data class provides anyway?

Also, if the intent of data classes is just to store values as is, why not prohibit var, setters, Unit methods, the init block, etc.? Those all allow you to manipulate data.

@KieferSkunk I think you misunderstand what Java interop means. It’s only about naturally interacting with structures generated from both languages seamlessly.

Also I completelly forgot about Java records before. I’m little suprised, but you can apparently overwrite a main constructor (not tested myself):

record Course(
	Long id,
	String title,
	String description,
	BigDecimal price
) {
	public static final BigDecimal DEFAULT_PRICE = BigDecimal.valueOf(100);
	
	public Course(Long id, String title, String description, BigDecimal price) {
		if (price.compareTo(BigDecimal.ZERO) < 0)
			throw new IllegalArgumentException("Price have to be greater than 0");
		this.id = id;
		this.title = title;
		this.description = description;
		this.price = price;
	}
}

I’m not sure if it’s a perfect approach. You can potentially manipulate data in constructor instead of assigning it straight from passed inputs. It also wouldn’t do well if we wanted a proper pattern matching in Java or Kotlin.

I don’t think that’s equivalent in this case. Kotlin prevents you from writing a secondary constructor with the same signature as the primary (nullability notwithstanding), which I think makes sense. A consumer wouldn’t be able to tell which of those constructors it should use in that situation.

I thought of a better way to explain this use-case that I believe should help in understanding the request. Many Java developers (and library consumers) depend on Jackson.databind (ObjectMapper) for JSON deserialization, especially in MVC environments. It’s very common for the JSON doc to be missing fields that are defined in the class, and unless the field is specifically mandatory, the consumer expects it to “just work”. By default, if the class being constructed at deserialization has a @JsonCreator constructor, rather than using setters to populate the object, Jackson will supply null (or a primitive’s default - e.g. false, 0, etc.) to any parameters that aren’t accounted for in the JSON.

This “just works” in Java because Java naturally makes everything either nullable or primitive (where there are documented default values). It’s actually harder to designate a field as mandatory in Java. And in cases where a field should have a default value when it’s not present in the JSON doc, a common practice is to do a null-check in the constructor and set the default value there, if you want to preserve immutability within the class. (Using a setter-based approach requires mutability.)

In Kotlin, this is reversed - immutability and non-nullability are default and standard (which I love). But this presents challenges for JSON-deserializable classes. If I have a library originally written in Java that’s intended for other users to work with, then when I migrate to Kotlin, I either need to preserve the existing contracts on these classes, or I need to instruct my consumers to update their code so that Jackson continues to work. The simplest way to do the latter is to register a KotlinModule in the ObjectMapper, but that will require the consumer to take a Kotlin-specific action (exposing implementation details of the library) when they otherwise wouldn’t need to do anything to continue using the newer version of the library. In other words, migrating to Kotlin can introduce breaking changes simply because of the language’s structure.

Alternatively, I have to go to much greater lengths to protect against that, which currently means I cannot use a data class for these objects even though I otherwise have no reason to use a regular class. If I want the auto-generated functions like equals() and toString(), I have to write them myself (or use IDEA’s macros to generate the code, but it still means there’s manual boilerplate code in my class). Meanwhile, I have to write a much more complex constructor, use var fields, and/or add extra class or field-scoped annotations to instruct Jackson how to handle every non-mandatory field.

My point here is that either an Elvis operator (or something similar) in the language itself, or something like the @NullDefaults annotation that kyay10 was working on, would make it extremely easy to do a smooth migration to Kotlin without forcing unnecessarily difficult workarounds for downstream consumers whose code I don’t control, or forcing breaking changes upon them. This would make Kotlin that much more elegant, I believe this request is in the spirit of preserving the language’s null safety, and it’s an opportunity to elevate this language above Java, Scala, etc… The ability to natively assign a default value when given a null is something a lot of people have been asking for for years in other languages, Java included.

I also made a request in a similar vein to the Lombok developers a while back that would have significantly improved the usability of one of their existing features. Their response was kinda similar to the initial response I got here, apparently born of the philosophy that “You should have designed it the right way to start with” - which completely ignores the possibility that I took over code I didn’t design but that we’re stuck with and are trying to improve. In this case, migrating to Kotlin presents an enormous improvement to our code base, but due to compatibility concerns, there are some places like this one where, frankly, Java does it much better.

That said, it’s JetBrains’s language, and their decision. I feel I’ve made my case - I’ll stop hounding you all about it. For now, I went with a solution that will work for us, and thankfully I do own all the code that has to interact with the updated classes, so I can make the necessary refactors, even though it will require those refactors in dozens of classes and literally more than a hundred unit tests. But I am well aware of how other library developers won’t be so lucky.

1 Like

If it’s just about the Jackson then it already supports default values out of the box:

data class HoledRequest(
	val string: String = "",
	val list: List<String> = emptyList()
)

fun main() {
	jacksonObjectMapper().readValue<HoledRequest>("{}").let(::println) // HoledRequest(string=, list=[])
}