Skip to content

Keep the values of Kotlin custom scalars without visible properties in arguments - #931

Merged
oryan-block merged 4 commits into
masterfrom
bugfix/926-kotlin-empty-scalars
Oct 24, 2026
Merged

oryan-block merged 4 commits into
masterfrom
bugfix/926-kotlin-empty-scalars

Conversation

@oryan-block

@oryan-block oryan-block commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Fixes #926

A custom scalar backed by a Kotlin class without visible properties lost its value on the way to the resolver:

class Token(private val value: String = "default")
data class TokenInput(val token: Token? = null)

tokenInput(input: { token: "input" }) got Token("default"), an Optional<Token> parameter got the same, and the same scalar inside an ObjectNode argument arrived as {}.

It's not the Kotlin module. Jackson only applies FAIL_ON_EMPTY_BEANS to classes without annotations. For an annotated class with no properties it builds a dummy serializer that writes {}, and every Kotlin class has @kotlin.Metadata. So the error that makes ArgumentConverter pass the value through as it is never fired, and {} was read back as a new instance. Java classes with a class annotation hit the same thing.

Now ArgumentConverter writes through a generator that fails when a bean serializer without properties closes an empty object, the same way an unannotated Java class fails. That sends the value down the existing embedded-value path, so the resolver gets the coerced instance (or a POJONode of it in an ObjectNode). Converting such a value to another class, or to a field declared as a Map or ObjectNode, still goes through {} like before, rather than failing.

Not covered: an Optional parameter of a scalar class without properties that's written with polymorphic type info (e.g. @JsonTypeInfo with a wrapper object) now fails where it used to convert, since the embedded value can't carry the type id. A plain or list parameter of the scalar skips the converter, and the same scalar inside an input object already failed on master. So it needs that exact combination. I don't think anyone does that, but it's there. A scalar like this written with a type id property or @JsonIdentityInfo still loses its value, same as on master. That's #932.

Release note: custom scalars backed by a Kotlin class without visible properties are passed to resolvers with their value, in input objects, Optional parameters and ObjectNode arguments.

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

The new case in CustomScalarInputTest fails on master (optionalToken=default, tokenInput=default, tokenNode={}) and passes here. A second case pins that conversions to another class and to fields declared as Map/ObjectNode give the same result as master. mvn clean verify passes (430 tests). Also checked through the installed jar in a separate consumer project: the input object case returns token default on master and token input with this change. A Kotlin scalar without properties converted to another class gives the same result as master.

Jackson skips FAIL_ON_EMPTY_BEANS for a class with annotations and writes it as {}. Every Kotlin class has @metadata, so a Kotlin scalar class without visible properties was read back as a new default instance, or as {} into an ObjectNode. The argument converter now fails on any bean written without properties, so the value is embedded like an unannotated Java class.
A custom scalar without visible properties is now embedded, and Map and JsonNode deserializers reject embedded values, so a Kotlin scalar class in a Map or ObjectNode field failed where it used to convert through {}.
@sonarqubecloud

Copy link
Copy Markdown

@oryan-block
oryan-block merged commit 2c19290 into master Oct 24, 2026
5 checks passed
@oryan-block
oryan-block deleted the bugfix/926-kotlin-empty-scalars branch October 24, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kotlin scalar classes without visible properties lose their value in arguments

1 participant