12

Let's take the class of a data class:

data class User(
    val userNumber: Int = -1,
    val name: String,
    val userGroups; List<String> = emptyList(),
    val screenName: String = "new-user"
)

When calling this function from Kotlin, it is pretty straightforward. I can simply use the named-argument syntax to do so. Calling from Java, I have to specify all values, or use the @JvmOverloads annotation, which generates the following constructors (in addition to the constructor that kotlin generates with the bit-mask for default values):

User(int userNumber, @NotNull String name, @NotNull List userGroups,
     @NotNull String screenName)
User(int userNumber, @NotNull String name, @NotNull List userGroups)
User(int userNumber, @NotNull String name)
User(@NotNull String name)

Now, if I want to create a User object in Java equivalent to User(name="John Doe", userGroups=listOf("admin", "super") I can't do it with the above constructors. I CAN however do it if I put val userNumber: Int = -1 at the end in the data class declaration (the generation of constructors seems to depend on the order the optional arguments are defined in). Which is fine, because expecting kotlin to generate all permutations is going to heavily bloat some classes.

The biggest problem that tools like Jackson simply don't work as they have no idea which constructor to use (and not like I can annotate one of the generated ones specially).

So, is there a way to generate a (single) constructor like:

User(Integer userNumber, String name, List<String> userGroups, String screenName) {
    this.userNumber = (userNumber == null) ? -1 : userNumber;
    this.userGroups = (userGroups == null) ? Collections.emptyList() : userGroups;
    //...
}

Currently I am using the above approach, but manually defining the constructors where I need them.

EDIT

I should clarify, creating a similar constructor doesn't work, obviously because both the signatures would clash on the JVM. This is what it would like in my case:

data class User(
    val userNumber: Int = -1,
    val name: String,
    val userGroups; List<String> = emptyList(),
    val screenName: String = "new-user"
) {
    companion object {
        @JvmStatic
        @JsonCreator
        fun constructionSupport(
            @JsonProperty("userNumber") userNumber : Int?,
            @JsonProperty("name") name : String,
            @JsonProperty("userGroups") userGroups : List<String>?,
            @JsonProperty("screenName") screenName : String?
        ) = User(
            userNumber = userNumber ?: -1,
            name = name,
            userGroups = userGroups ?: emptyList(),
            screenName = screenName ?: "new-user"
        )
    }
}

Also note the redundancy where I have to write the default values for the properties twice. I Now that I look at it, I doubt there exists a solution for this. Maybe this is a good use-case for a kapt based side-project of mine :)

3
  • 1
    Are you using jackson-module-kotlin(github.com/FasterXML/jackson-module-kotlin)? I think it can deal with constructors with default parameters. Not sure about your issue with Java though. Maybe use secondary constructors? Commented Aug 17, 2017 at 13:22
  • I don't actually have a problem with Java, only with jackson. For java you are right, I can simply use secondary constructors (technically, the static method above is akin to a secondary constructor). Will checkout jackson-module-kotlin though... Commented Aug 17, 2017 at 13:54
  • I've been expanding my constructor with this pattern and then creating a nested builder class for use from the Java side. It's definitely an unfortunate amount of boilerplate to create, but it is extremely clean to use. Commented Mar 21 at 21:04

2 Answers 2

0

Better solution is to add possibility to library understand Kotlin functional. For example, for Jackson exists jackson-module-kotlin. With this library we can use default arguments in data classes.

Example:

data class User(
        val userNumber: Int = -1,
        val name: String,
        val userGroups: List<String> = emptyList(),
        val screenName: String = "new-user"
)

fun main(args: Array<String>) {
    val objectMapper = ObjectMapper()
            .registerModule(KotlinModule())

    val testUser = User(userNumber = 5, name = "someName")

    val stringUser = objectMapper.writeValueAsString(testUser)
    println(stringUser)

    val parsedUser = objectMapper.readValue<User>(stringUser)
    println(parsedUser)

    assert(testUser == parsedUser) {
        println("something goes wrong")
    }
}
Sign up to request clarification or add additional context in comments.

Comments

-1

After kicking this around for a minute, I think I found a solution that may work well here. Simply define a top level function in the same source file, that will build the object. Perhaps like so:

fun build_user(userNumber: Int?, name: String, userGroups: List<String>?, screenName: String?) : User {
  return User(if(userNumber !== null) userNumber else -1, name, if(userGroups !== null) userGroups else emptyList(),
    if(screenName !== null) screenName else "new-user")
}

Then when you need it, you simply call it from Java:

User user = UserKt.build_user(null, "Hello", null, "Porterhouse Steak");
System.out.println(user);

Output from the example:

User(userNumber=-1, name=Hello, userGroups=[], screenName=Porterhouse Steak)

The method is somewhere between a constructor and a builder. It beats hammering out a full-blown Builder object, and avoids cluttering your data class with unnecessary Java-interop glue code messiness.

See Package Level Functions for more information.

2 Comments

If you check my question (the edit part), this is exactly what I am doing. Rather than having a top-level function, I simply have a static function within the class. That allows me to annotate with @JsonCreator for jackson, and also use this from Java. The problem is the amount of redundancy that this creates and the fact that I am essentially re-writing the constructor and it also now is my responsibility to make sure that the default values specified at these two points are the same.
Can't you just remove the defaults from the primary constructor? The companion factory method should be your sole route for construction with optional params.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.