diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..1036ca72e
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+github: [k163377, cowtowncoder]
+tidelift: maven/com.fasterxml.jackson.module:jackson-module-kotlin
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 4957ffa0a..c93e89e1d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -8,10 +8,20 @@ body:
id: pre-check
attributes:
label: Search before asking
- description: "Please search [issues](https://github.com/FasterXML/jackson-module-kotlin/issues) to check if your issue has already been reported."
+ description: |-
+ Please search [issues](https://github.com/FasterXML/jackson-module-kotlin/issues) to check if your issue has already been reported.
+ Check [all value class labeled issues](https://github.com/FasterXML/jackson-module-kotlin/issues?q=is%3Aopen+is%3Aissue+label%3A%22value+class%22), especially if the problem is related to a value class.
+ Also, KotlinModule is only an extension of databind, so it is not an appropriate place to report problems with databind or other modules.
+ Please try to ensure that the problem occurs only in Kotlin, and not regular Java objects.
options:
- label: "I searched in the [issues](https://github.com/FasterXML/jackson-module-kotlin/issues) and found nothing similar."
required: true
+ - label: "I have confirmed that the same problem is not reproduced if I exclude the KotlinModule."
+ required: true
+ - label: "I searched in the [issues of databind](https://github.com/FasterXML/jackson-databind/issues) and other modules used and found nothing similar."
+ required: false
+ - label: "I have confirmed that the problem does not reproduce in Java and only occurs when using Kotlin and KotlinModule."
+ required: false
- type: textarea
id: bug-description
attributes:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..2390d8c80
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/dep_build_v2.yml b/.github/workflows/dep_build_v2.yml
new file mode 100644
index 000000000..c3c0d45d8
--- /dev/null
+++ b/.github/workflows/dep_build_v2.yml
@@ -0,0 +1,35 @@
+name: Re-build on jackson-databind v2 push
+on:
+ repository_dispatch:
+ types: [jackson-databind-pushed]
+ # just for testing
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java_version: ['8', '17', '21', '24']
+ # Versions need to align with ones in 'main.yml' workflow
+ kotlin_version: ['2.0.21', '2.1.21', '2.2.0']
+ env:
+ JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: 2.x
+ - name: Set up JDK
+ uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
+ with:
+ distribution: 'temurin'
+ java-version: ${{ matrix.java_version }}
+ cache: 'maven'
+ - name: Build and test
+ run: ./mvnw -B -ff -ntp -Dversion.kotlin=${{ matrix.kotlin_version }} clean verify
+
+# No recursive rebuild (yet?)
diff --git a/.github/workflows/dep_build_v3.yml b/.github/workflows/dep_build_v3.yml
new file mode 100644
index 000000000..a50f0f4de
--- /dev/null
+++ b/.github/workflows/dep_build_v3.yml
@@ -0,0 +1,31 @@
+name: Re-build on jackson-databind v3 push
+on:
+ repository_dispatch:
+ types: [jackson-databind-pushed-v3]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java_version: ['17', '21', '24']
+ # Versions need to align with ones in 'main.yml' workflow
+ kotlin_version: ['2.0.21', '2.1.21', '2.2.0']
+ env:
+ JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ ref: 3.x
+ - name: Set up JDK
+ uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
+ with:
+ distribution: 'temurin'
+ java-version: ${{ matrix.java_version }}
+ cache: 'maven'
+ - name: Build and test
+ run: ./mvnw -B -ff -ntp -Dversion.kotlin=${{ matrix.kotlin_version }} clean verify
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d3db2e4cc..d179795b7 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,45 +1,42 @@
name: Build and Deploy Snapshot
on:
push:
- branches:
- - master
- - "3.0"
- - "2.16"
+ branches: ['2.*']
paths-ignore:
- "README.md"
- "release-notes/*"
pull_request:
- branches:
- - master
- - "3.0"
- - "2.16"
paths-ignore:
- "README.md"
- "release-notes/*"
permissions:
contents: read
-
+
jobs:
build:
- runs-on: ${{ matrix.os }}
+ runs-on: ubuntu-latest
strategy:
fail-fast: false
+ max-parallel: 5
matrix:
- java_version: ['8', '11', '17']
- kotlin_version: ['1.6.21', '1.7.20', '1.8.22', '1.9.0']
- os: ['ubuntu-20.04']
+ java_version: ['8', '11', '17', '21', '24']
+ kotlin_version: ['2.0.21', '2.1.21', '2.2.0']
+ include:
+ - java_version: '8'
+ kotlin_version: '2.0.21'
+ release_build: 'R'
env:
JAVA_OPTS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK
- uses: actions/setup-java@v3
+ uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'temurin'
java-version: ${{ matrix.java_version }}
cache: 'maven'
- server-id: sonatype-nexus-snapshots
+ server-id: central-snapshots
server-username: CI_DEPLOY_USERNAME
server-password: CI_DEPLOY_PASSWORD
# See https://github.com/actions/setup-java/blob/v2/docs/advanced-usage.md#Publishing-using-Apache-
@@ -54,11 +51,11 @@ jobs:
run: ./mvnw -B -q -ff -ntp -Dversion.kotlin=${{ matrix.kotlin_version }} surefire:test
- name: Extract project Maven version
id: projectVersion
- run: echo "version=$(./mvnw org.apache.maven.plugins:maven-help-plugin:3.3.0:evaluate -DforceStdout -Dexpression=project.version -q)" >> $GITHUB_OUTPUT
+ run: echo "version=$(./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.1:evaluate -DforceStdout -Dexpression=project.version -q)" >> $GITHUB_OUTPUT
- name: Deploy snapshot
- if: github.event_name != 'pull_request' && matrix.java_version == '8' && matrix.kotlin_version == '1.5.32' && endsWith(steps.projectVersion.outputs.version, '-SNAPSHOT')
+ if: ${{ github.event_name != 'pull_request' && matrix.release_build && endsWith(steps.projectVersion.outputs.version, '-SNAPSHOT') }}
env:
- CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }}
- CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }}
+ CI_DEPLOY_USERNAME: ${{ secrets.CENTRAL_DEPLOY_USERNAME }}
+ CI_DEPLOY_PASSWORD: ${{ secrets.CENTRAL_DEPLOY_PASSWORD }}
# MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: ./mvnw -B -q -ff -DskipTests -ntp source:jar deploy
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index ca5ab4bab..b9b1153ae 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -14,5 +14,5 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
diff --git a/README.md b/README.md
index 2a828e57a..ebbde1488 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
-[](https://kotlinlang.org) [](https://slack.kotlinlang.org/)
+[](https://mvnrepository.com/artifact/com.fasterxml.jackson.module/jackson-module-kotlin)
+[](./release-notes/VERSION-2.x)
+[](https://tidelift.com/subscription/pkg/maven-com-fasterxml-jackson-module-jackson-module-kotlin?utm_source=maven-com-fasterxml-jackson-module-jackson-module-kotlin&utm_medium=referral&utm_campaign=readme)
+[](https://slack.kotlinlang.org/)
# Overview
@@ -10,15 +13,15 @@ and those with secondary constructors or static factories are also supported.
# Status
-* release `2.16.0` (for Jackson `2.16.x`) [](https://github.com/FasterXML/jackson-module-kotlin/actions?query=branch%3A2.16)
-* release `2.15.3` (for Jackson `2.15.x`) [](https://github.com/FasterXML/jackson-module-kotlin/actions?query=branch%3A2.15)
-* release `2.14.3` (for Jackson `2.14.x`) [](https://github.com/FasterXML/jackson-module-kotlin/actions?query=branch%3A2.14)
+* release `2.19.0` (for Jackson `2.19.x`) [](https://github.com/FasterXML/jackson-module-kotlin/actions?query=branch%3A2.19)
+* release `2.18.3` (for Jackson `2.18.x`) [](https://github.com/FasterXML/jackson-module-kotlin/actions?query=branch%3A2.18)
+* release `2.17.3` (for Jackson `2.17.x`) [](https://github.com/FasterXML/jackson-module-kotlin/actions?query=branch%3A2.17)
Releases require that you have included Kotlin stdlib and reflect libraries already.
Gradle:
```
-implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.16.+"
+implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.19.+"
```
Maven:
@@ -26,7 +29,7 @@ Maven:
com.fasterxml.jackson.modulejackson-module-kotlin
- 2.16.0
+ 2.19.0
```
@@ -55,28 +58,8 @@ val mapper = jsonMapper {
}
```
-
- Jackson versions prior to 2.10–2.11
-
-```kotlin
-import com.fasterxml.jackson.databind.json.JsonMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-...
-val mapper = JsonMapper.builder().addModule(KotlinModule()).build()
-```
-
-
-
-
- Jackson versions prior to 2.10
-
-```kotlin
-import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-...
-val mapper = ObjectMapper().registerModule(KotlinModule())
-```
-
+In 2.17 and later, the `jacksonObjectMapper {}` and `registerKotlinModule {}` lambdas allow configuration for `KotlinModule`.
+See [#Configuration](#Configuration) for details on the available configuration items.
A simple data class example:
```kotlin
@@ -129,10 +112,12 @@ println(arrayNode.toString()) // ["foo",true,1,1.0,"YmFy"]
Different `kotlin-core` versions are supported by different Jackson Kotlin module minor versions.
Here is an incomplete list of supported versions:
-* Jackson 2.16.x: Kotlin-core 1.6 - 1.9
-* Jackson 2.15.x: Kotlin-core 1.5 - 1.8
-* Jackson 2.14.x: Kotlin-core 1.4 - 1.8
-* Jackson 2.13.x: Kotlin-core 1.4 - 1.7
+* Jackson 2.20.x: Kotlin-core 2.0 - 2.2
+* Jackson 2.19.x: Kotlin-core 1.9 - 2.1
+* Jackson 2.18.x: Kotlin-core 1.8 - 2.1
+* Jackson 2.17.x: Kotlin-core 1.7 - 2.0
+
+Please note that the versions supported by 2.17 are tentative and may change depending on the release date.
## Android
Supported Android SDK versions are determined by `jackson-databind`.
@@ -145,12 +130,12 @@ Any fields not present in the constructor will be set after the constructor call
An example of these concepts:
```kotlin
- @JsonInclude(JsonInclude.Include.NON_EMPTY)
- class StateObjectWithPartialFieldsInConstructor(val name: String, @JsonProperty("age") val years: Int) {
- @JsonProperty("address") lateinit var primaryAddress: String // set after construction
- var createdDt: DateTime by Delegates.notNull() // set after construction
- var neverSetProperty: String? = null // not in JSON so must be nullable with default
- }
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+class StateObjectWithPartialFieldsInConstructor(val name: String, @JsonProperty("age") val years: Int) {
+ @JsonProperty("address") lateinit var primaryAddress: String // set after construction
+ var createdDt: DateTime by Delegates.notNull() // set after construction
+ var neverSetProperty: String? = null // not in JSON so must be nullable with default
+}
```
Note that using `lateinit` or `Delegates.notNull()` will ensure that the value is never `null` when read, while letting it be instantiated after the construction of the class.
@@ -164,6 +149,7 @@ Note that using `lateinit` or `Delegates.notNull()` will ensure that the value i
* `kotlin.Metadata` annotations may be stripped, preventing deserialization. Add a proguard rule to keep the `kotlin.Metadata` class: `-keep class kotlin.Metadata { *; }`
* If you're getting `java.lang.ExceptionInInitializerError`, you may also need: `-keep class kotlin.reflect.** { *; }`
* If you're still running into problems, you might also need to add a proguard keep rule for the specific classes you want to (de-)serialize. For example, if all your models are inside the package `com.example.models`, you could add the rule `-keep class com.example.models.** { *; }`
+ * Also, please refer to [this page](https://github.com/FasterXML/jackson-docs/wiki/JacksonOnAndroid) for settings related to `jackson-databind`.
# Support for Kotlin Built-in classes
@@ -176,6 +162,9 @@ These Kotlin classes are supported with the following fields for serialization/d
* CharRange _(start, end)_
* LongRange _(start, end)_
+Deserialization for `value class` is also supported since 2.17.
+Please refer to [this page](./docs/value-class-support.md) for more information on using `value class`, including serialization.
+
(others are likely to work, but may not be tuned for Jackson)
# Sealed classes without @JsonSubTypes
@@ -184,11 +173,11 @@ at compile-time to Kotlin. This makes `com.fasterxml.jackson.annotation.JsonSubT
A `com.fasterxml.jackson.annotation.@JsonTypeInfo` annotation at the base-class is still necessary.
```kotlin
- @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
- sealed class SuperClass{
- class A: SuperClass()
- class B: SuperClass()
- }
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
+sealed class SuperClass{
+ class A: SuperClass()
+ class B: SuperClass()
+}
...
val mapper = jacksonObjectMapper()
@@ -199,11 +188,10 @@ when(root){
}
```
-
# Configuration
The Kotlin module may be given a few configuration parameters at construction time;
-see the [inline documentation](https://github.com/FasterXML/jackson-module-kotlin/blob/master/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt)
+see the [inline documentation](https://github.com/FasterXML/jackson-module-kotlin/blob/2.19/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt)
for details on what options are available and what they do.
```kotlin
@@ -266,9 +254,9 @@ See the [main Jackson contribution guidelines](https://github.com/FasterXML/jack
If you are going to write code, choose the appropriate base branch:
-- `2.15` for bugfixes against the current stable version
-- `2.16` for additive functionality & features or [minor](https://semver.org), backwards compatible changes to existing behavior to be included in the next minor version release
-- `master` for significant changes to existing behavior, which will be part of Jackson 3.0
+- `2.19` for bugfixes against the current stable version
+- `2.x` for additive functionality & features or [minor](https://semver.org), backwards compatible changes to existing behavior to be included in the next minor version release
+- `3.x` for significant changes to existing behavior, which will be part of Jackson 3.0
### Failing tests
diff --git a/docs/value-class-handling.md b/docs/value-class-handling.md
new file mode 100644
index 000000000..13218ed85
--- /dev/null
+++ b/docs/value-class-handling.md
@@ -0,0 +1,134 @@
+This is a document that summarizes how `value class` is handled in `kotlin-module`.
+
+# Annotation assigned to a property (parameter)
+In `Kotlin`, annotations on properties will be assigned to the parameters of the primary constructor.
+On the other hand, if the parameter contains a `value class`, this annotation will not work.
+See #651 for details.
+
+# Serialize
+Serialization is performed as follows
+
+1. If the value is unboxed in the getter of a property, re-box it
+2. Serialization is performed by the serializer specified for the class or by the default serializer of `kotlin-module`
+
+## Re-boxing of value
+Re-boxing is handled by `KotlinAnnotationIntrospector#findSerializationConverter`.
+
+The properties re-boxed here are handled as if the type of the getter was `value class`.
+This allows the `JsonSerializer` specified for the mapper, class and property to work.
+
+### Edge case on `value class` that wraps `null`
+If the property is non-null and the `value class` that is the value wraps `null`,
+then the value is re-boxed by `KotlinAnnotationIntrospector#findNullSerializer`.
+This is the case for serializing `Dto` as follows.
+
+```kotlin
+@JvmInline
+value class WrapsNullable(val v: String?)
+
+data class Dto(val value: WrapsNullable = WrapsNullable(null))
+```
+
+In this case, features like the `JsonSerialize` annotation will not work as expected due to the difference in processing paths.
+
+## Default serializers with `kotlin-module`
+Default serializers for boxed values are implemented in `KotlinSerializers`.
+There are two types: `ValueClassUnboxSerializer` and `ValueClassSerializer.StaticJsonValue`.
+
+The former gets the value by unboxing and the latter by executing the method with the `JsonValue` annotation.
+The serializer for the retrieved value is then obtained and serialization is performed.
+
+# Deserialize
+Deserialization is performed as follows
+
+1. Get `KFunction` from a non-synthetic constructor (if the constructor is a creator)
+2. If it is unboxed on a parameter, refine it to a boxed type
+3. `value class` is deserialized by `Jackson` default handling or by `kotlin-module` deserializer
+4. Instantiation is done by calling `KFunction`
+
+The special `JsonDeserializer`, `WrapsNullableValueClassDeserializer`, is described in the [section on instantiation](#Instantiation).
+
+## Get `KFunction` from non-synthetic constructor
+Constructor with `value class` parameters compiles into a `private` non-synthesized constructor and a synthesized constructor.
+
+A `KFunction` is inherently interconvertible with any constructor or method in a `Java` reflection.
+In the case of a constructor with a `value class` parameter, it is the synthetic constructor that is interconvertible.
+
+On the other hand, `Jackson` does not handle synthetic constructors.
+Therefore, `kotlin-module` needs to get `KFunction` from a `private` non-synthetic constructor.
+
+This acquisition process is implemented as a `valueClassAwareKotlinFunction` in `ReflectionCache.kt`.
+
+## Refinement to boxed type
+Refinement to a boxed type is handled by `KotlineNamesAnnotationIntrospector#refineDeserializationType`.
+Like serialization, the parameters refined here are handled as if the type of the parameter was `value class`.
+
+This will cause the result of reading from the `PropertyValueBuffer` with `ValueInstantiator#createFromObjectWith` to be the boxed value.
+
+## Deserialization of `value class`
+Deserialization of `value class` may be handled by default by `Jackson` or by `kotlin-module`.
+
+### by `Jackson`
+If a custom `JsonDeserializer` is set or a special `JsonCreator` is defined,
+deserialization of the `value class` is handled by `Jackson` just like a normal class.
+The special `JsonCreator` is a factory function that is configured to return the `value class` in bytecode.
+
+The special `JsonCreator` is handled in exactly the same way as a regular class.
+That is, it does not have the restrictions that the mode is fixed to `DELEGATING`
+or that it cannot have multiple arguments.
+This can be defined by setting the return value to `nullable`, for example
+
+```kotlin
+@JvmInline
+value class PrimitiveMultiParamCreator(val value: Int) {
+ companion object {
+ @JvmStatic
+ @JsonCreator
+ fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
+ PrimitiveMultiParamCreator(first + second)
+ }
+}
+```
+
+### by `kotlin-module`
+Deserialization using constructors or factory functions that return unboxed value in bytecode
+is handled by the `WrapsNullableValueClassBoxDeserializer` that defined in `KotlinDeserializer.kt`.
+
+They must always have a parameter size of 1, like `JsonCreator` with `DELEGATING` mode specified.
+Note that the `kotlin-module` proprietary implementation raises an `InvalidDefinitionException`
+if the parameter size is greater than 2.
+
+## Instantiation
+Instantiation by calling `KFunction` obtained from a constructor or factory function is done with `KotlinValueInstantiator#createFromObjectWith`.
+
+Boxed values are required as `KFunction` arguments, but since the `value class` is read as a boxed value as described above,
+basic processing is performed as in a normal class.
+However, there is special processing for the edge case described below.
+
+### Edge case on `value class` that wraps nullable
+If the parameter type is `value class` and non-null, which wraps nullable, and the value on the JSON is null,
+the wrapped null is expected to be read as the value.
+
+```kotlin
+@JvmInline
+value class WrapsNullable(val value: String?)
+
+data class Dto(val wrapsNullable: WrapsNullable)
+
+val mapper = jacksonObjectMapper()
+
+// serialized: {"wrapsNullable":null}
+val json = mapper.writeValueAsString(Dto(WrapsNullable(null)))
+// expected: Dto(wrapsNullable=WrapsNullable(value=null))
+val deserialized = mapper.readValue(json)
+```
+
+In `kotlin-module`, a special `JsonDeserializer` named `WrapsNullableValueClassDeserializer` was introduced to support this.
+This deserializer has a `boxedNullValue` property,
+which is referenced in `KotlinValueInstantiator#createFromObjectWith` as appropriate.
+
+I considered implementing it with the traditional `JsonDeserializer#getNullValue`,
+but I chose to implement it as a special property because of inconsistencies that could not be resolved
+if all cases were covered in detail in the prototype.
+Note that this property is referenced by `KotlinValueInstantiator#createFromObjectWith`,
+so it will not work when deserializing directly.
diff --git a/docs/value-class-support.md b/docs/value-class-support.md
new file mode 100644
index 000000000..a42786c66
--- /dev/null
+++ b/docs/value-class-support.md
@@ -0,0 +1,160 @@
+`jackson-module-kotlin` supports many use cases of `value class` (`inline class`).
+This page summarizes the basic policy and points to note regarding the use of the `value class`.
+
+For technical details on `value class` handling, please see [here](./value-class-handling.md).
+
+# Note on the use of `value class`
+`jackson-module-kotlin` supports the `value class` for many common use cases, both serialization and deserialization.
+However, full compatibility with normal classes (e.g. `data class`) is not achieved.
+In particular, there are many edge cases for the `value class` that wraps nullable.
+
+The cause of this difference is that the `value class` itself and the functions that use the `value class` are
+compiled into bytecodes that differ significantly from the normal classes.
+Due to this difference, some cases cannot be handled by basic `Jackson` parsing, which assumes `Java`.
+Known issues related to `value class` can be found [here](https://github.com/FasterXML/jackson-module-kotlin/issues?q=is%3Aissue+is%3Aopen+label%3A%22value+class%22).
+
+In addition, one of the features of the `value class` is improved performance,
+but when using `Jackson` (not only `Jackson`, but also other libraries that use reflection),
+the performance is rather reduced.
+This can be confirmed from [kogera-benchmark](https://github.com/ProjectMapK/kogera-benchmark?tab=readme-ov-file#comparison-of-normal-class-and-value-class).
+
+For these reasons, we recommend careful consideration when using `value class`.
+
+# Basic handling of `value class`
+A `value class` is basically treated like a value.
+
+For example, the serialization of `value class` is as follows
+
+```kotlin
+@JvmInline
+value class Value(val value: Int)
+
+val mapper = jacksonObjectMapper()
+mapper.writeValueAsString(Value(1)) // -> 1
+```
+
+This is different from the `data class` serialization result.
+
+```kotlin
+data class Data(val value: Int)
+
+mapper.writeValueAsString(Data(1)) // -> {"value":1}
+```
+
+The same policy applies to deserialization.
+
+This policy was decided with reference to the behavior as of `jackson-module-kotlin 2.14.1` and [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/value-classes.md#serializable-value-classes).
+However, these are just basic policies, and the behavior can be overridden with `JsonSerializer` or `JsonDeserializer`.
+
+# Notes on customization
+As noted above, the content associated with the `value class` is not fully compatible with the normal class.
+Here is a summary of the customization considerations for such contents.
+
+## Annotation
+Annotations assigned to parameters in a primary constructor that contains `value class` as a parameter will not work.
+It must be assigned to a field or getter.
+
+```kotlin
+data class Dto(
+ @JsonProperty("vc") // does not work
+ val p1: ValueClass,
+ @field:JsonProperty("vc") // does work
+ val p2: ValueClass
+)
+```
+
+See #651 for details.
+
+## On serialize
+### JsonValue
+The `JsonValue` annotation is supported.
+
+```kotlin
+@JvmInline
+value class ValueClass(val value: UUID) {
+ @get:JsonValue
+ val jsonValue get() = value.toString().filter { it != '-' }
+}
+
+// -> "e5541a61ac934eff93516eec0f42221e"
+mapper.writeValueAsString(ValueClass(UUID.randomUUID()))
+```
+
+### JsonSerializer
+The `JsonSerializer` basically supports the following methods:
+registering to `ObjectMapper`, giving the `JsonSerialize` annotation.
+Also, although `value class` is basically serialized as a value,
+but it is possible to serialize `value class` like an object by using `JsonSerializer`.
+
+```kotlin
+@JvmInline
+value class ValueClass(val value: UUID)
+
+class Serializer : StdSerializer(ValueClass::class.java) {
+ override fun serialize(value: ValueClass, gen: JsonGenerator, provider: SerializerProvider) {
+ val uuid = value.value
+ val obj = mapOf(
+ "mostSignificantBits" to uuid.mostSignificantBits,
+ "leastSignificantBits" to uuid.leastSignificantBits
+ )
+
+ gen.writeObject(obj)
+ }
+}
+
+data class Dto(
+ @field:JsonSerialize(using = Serializer::class)
+ val value: ValueClass
+)
+
+// -> {"value":{"mostSignificantBits":-6594847211741032479,"leastSignificantBits":-5053830536872902344}}
+mapper.writeValueAsString(Dto(ValueClass(UUID.randomUUID())))
+```
+
+Note that specification with the `JsonSerialize` annotation will not work
+if the `value class` wraps null and the property definition is non-null.
+
+## On deserialize
+### JsonDeserializer
+Like `JsonSerializer`, `JsonDeserializer` is basically supported.
+However, it is recommended that `WrapsNullableValueClassDeserializer` be inherited and implemented as a
+deserializer for `value class` that wraps nullable.
+
+This deserializer is intended to make the deserialization result be a wrapped null if the parameter definition
+is a `value class` that wraps nullable and non-null, and the value on the `JSON` is null.
+An example implementation is shown below.
+
+```kotlin
+@JvmInline
+value class ValueClass(val value: String?)
+
+class Deserializer : WrapsNullableValueClassDeserializer(ValueClass::class) {
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ValueClass {
+ TODO("Not yet implemented")
+ }
+
+ override fun getBoxedNullValue(): ValueClass = WRAPPED_NULL
+
+ companion object {
+ private val WRAPPED_NULL = ValueClass(null)
+ }
+}
+```
+
+### JsonCreator
+`JsonCreator` basically behaves like a `DELEGATING` mode.
+Note that defining a creator with multiple arguments will result in a runtime error.
+
+As a workaround, a factory function defined in bytecode with a return value of `value class` can be deserialized in the same way as a normal creator.
+
+```kotlin
+@JvmInline
+value class PrimitiveMultiParamCreator(val value: Int) {
+ companion object {
+ @JvmStatic
+ @JsonCreator
+ fun creator(first: Int, second: Int): PrimitiveMultiParamCreator? =
+ PrimitiveMultiParamCreator(first + second)
+ }
+}
+```
diff --git a/pom.xml b/pom.xml
index c542f4f57..a822d5e69 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,12 +8,12 @@
com.fasterxml.jacksonjackson-base
- 2.16.3-SNAPSHOT
+ 2.20.0-SNAPSHOTcom.fasterxml.jackson.modulejackson-module-kotlinjackson-module-kotlin
- 2.16.3-SNAPSHOT
+ 2.20.0-SNAPSHOTbundleAdd-on module for Jackson (https://github.com/FasterXML/jackson/) to support
Kotlin language, specifically introspection of method/constructor parameter names,
@@ -53,7 +53,7 @@
scm:git:git@github.com:FasterXML/jackson-module-kotlin.gitscm:git:git@github.com:FasterXML/jackson-module-kotlin.githttps://github.com/FasterXML/jackson-module-kotlin
- jackson-module-kotlin-2.16.0-rc1
+ jackson-module-kotlin-2.18.0-rc1
@@ -62,8 +62,8 @@
1.81.8
- 1.6.21
-
+ 2.0.21
+
com/fasterxml/jackson/module/kotlin${project.groupId}.kotlin
@@ -109,16 +109,11 @@
compile
-
-
- junit
- junit
- ${version.junit}
- test
-
+
+
org.jetbrains.kotlin
- kotlin-test-junit
+ kotlin-test-junit5${version.kotlin}test
@@ -127,6 +122,11 @@
jackson-dataformat-xmltest
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-csv
+ test
+ com.fasterxml.jackson.datatype
@@ -136,7 +136,6 @@
- ${project.basedir}/src/main/kotlin${project.basedir}/src/test/kotlin
@@ -151,6 +150,13 @@
compile
+
+
+ ${project.basedir}/target/generated-sources
+ ${project.basedir}/src/main/java
+ ${project.basedir}/src/main/kotlin
+
+
@@ -161,7 +167,6 @@
- -Xinline-classes
@@ -170,6 +175,15 @@
org.apache.maven.pluginsmaven-surefire-plugin
+
+
+
+ com.fasterxml.jackson.module.kotlin.**
+
+
@@ -182,6 +196,11 @@
+
+
+ org.cyclonedx
+ cyclonedx-maven-plugin
+ org.apache.maven.pluginsmaven-compiler-plugin
@@ -200,25 +219,25 @@
- de.jjohannes
+ org.gradlexgradle-module-metadata-maven-plugincom.github.siom79.japicmpjapicmp-maven-plugin
- 0.15.7
+ 0.23.1com.fasterxml.jackson.modulejackson-module-kotlin
-
-
- 2.15.2
+
+
+ 2.19.0jar
@@ -231,13 +250,27 @@
truetrue
-
-
- com.fasterxml.jackson.module.kotlin.KotlinModule
-
- com.fasterxml.jackson.module.kotlin.KotlinNamesAnnotationIntrospector
- com.fasterxml.jackson.module.kotlin.KotlinAnnotationIntrospector
- com.fasterxml.jackson.module.kotlin.KotlinDeserializers
+
+
+
+ com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException#MissingKotlinParameterException(kotlin.reflect.KParameter,java.io.Closeable,java.lang.String)
+
+
+ com.fasterxml.jackson.module.kotlin.WrapsNullableValueClassBoxDeserializer
+
+ com.fasterxml.jackson.module.kotlin.ValueClassUnboxKeySerializer
+ com.fasterxml.jackson.module.kotlin.KotlinKeySerializersKt
+ com.fasterxml.jackson.module.kotlin.ValueClassSerializer
+
+
+ com.fasterxml.jackson.module.kotlin.KotlinKeySerializers#KotlinKeySerializers()
+
+
+ com.fasterxml.jackson.module.kotlin.KotlinSerializers#KotlinSerializers()
+
+ com.fasterxml.jackson.module.kotlin.ValueClassStaticJsonKeySerializer
+ com.fasterxml.jackson.module.kotlin.ValueClassBoxConverter
+ com.fasterxml.jackson.module.kotlin.ValueClassKeyDeserializer
@@ -250,19 +283,37 @@
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ generate-sources
+
+ add-source
+
+
+
+ src/main/kotlin
+
+
+
+
+
-
-
- snapshots-repo
- https://oss.sonatype.org/content/repositories/snapshots
-
- false
-
-
- true
-
-
-
+
+
+
+
+ central-snapshots
+ Sonatype Central Portal (snapshots)
+ https://central.sonatype.com/repository/maven-snapshots
+ false
+ true
+
+
diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index f650a00d4..cb72c754c 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -15,9 +15,103 @@ Authors:
Contributors:
-# 2.17.0 (not yet released)
+# 2.20.0 (not yet released)
-# 2.16.1 (not yet released)
+WrongWrong (@k163377)
+* #1020: Fixed old StrictNullChecks to throw exceptions similar to those thrown by new StrictNullChecks
+* #1018: Use MethodHandle in processing related to value class
+* #969: Cleanup of deprecated contents
+* #967: Update settings for 2.20
+
+# 2.19.1 (not yet released)
+
+WrongWrong (@k163377)
+* #986: Optimize imports
+
+# 2.19.0 (24-Apr-2025)
+
+WrongWrong (@k163377)
+* #959: Add extensions for configOverride
+* #954: Replace BooleanTriState with OptBoolean
+* #944: Fixed to use common util for Member accessibility override
+* #937: Added type match check to read functions
+
+Tatu Saloranta (@cowtowncoder)
+* #889: Upgrade kotlin dep to 1.9.25 (from 1.9.24)
+
+WrongWrong (@k163377)
+* #930: Add tests for #917
+* #929: Bug fixes to hasRequiredMarker and added isRequired considerations
+* #914: Add test case to serialize Nothing? (for #314)
+* #910: Add default KeyDeserializer for value class
+* #885: Performance improvement of strictNullChecks
+* #884: Changed the base class of MissingKotlinParameterException to InvalidNullException
+* #878: Fix for #876
+* #868: Added test case for FAIL_ON_NULL_FOR_PRIMITIVES
+* #866: Upgrade to JUnit5
+* #861: Update Kotlin to 1.9.24
+* #858: Refactor findDefaultCreator
+* #839: Remove useKotlinPropertyNameForGetter and unify with kotlinPropertyNameAsImplicitName
+* #835: Remove old SingletonSupport class and unified with KotlinFeature.SingletonSupport
+
+# 2.18.4 (not yet released)
+
+WrongWrong (@k163377)
+* #923: Fixed hasRequiredMarker to only process content defined in Kotlin
+* #920: Minor refactors that do not affect behavior
+
+# 2.18.3 (28-Feb-2025)
+
+WrongWrong (@k163377)
+* #908: Additional fixes related to #904.
+* #904: Fixed an error when serializing a `value class` that wraps a `Map`
+* #900: Fixed an issue where some tests were not running
+
+# 2.18.0 (26-Sep-2024)
+
+WrongWrong (@k163377)
+* #883: Raise the deprecation level to error for the MissingKotlinParameterException secondary constructor
+* #869: Replaced Enum.values with Enum.entries
+* #818: Optimize the search process for creators
+* #817: Fixed nullability of convertValue function argument
+* #782: Organize deprecated contents
+* #542: Remove meaningless checks and properties in KNAI
+
+# 2.17.2 (05-Jul-2024)
+
+WrongWrong (@k163377)
+* #799: Fixed problem with code compiled with 2.17.x losing backward compatibility.
+
+# 2.17.1 (04-May-2024)
+
+WrongWrong (@k163377)
+* #776: Delete Duration conversion that was no longer needed
+* #779: Fixed to not process constructors of Java classes
+
+# 2.17.0
+
+WrongWrong (@k163377)
+* #768: Added value class deserialization support.
+* #763: Minor refactoring to support value class in deserialization.
+* #760: Improved processing related to parameter parsing on Kotlin.
+* #759: Organize internal commons.
+* #758: Deprecated SingletonSupport.
+* #755: Changes in constructor invocation and argument management.
+* #752: Fix KDoc for KotlinModule.
+* #751: Marked useKotlinPropertyNameForGetter as deprecated.
+* #747: Improved performance related to KotlinModule initialization and setupModule.
+* #746: The KotlinModule#serialVersionUID is set to private.
+* #745: Modified isKotlinClass determination method.
+* #744: API deprecation update for KotlinModule.
+* #743: Fix handling of vararg deserialization.
+* #742: Minor performance improvements to NullToEmptyCollection/Map.
+* #741: Changed to allow KotlinFeature to be set in the function that registers a KotlinModule.
+* #740: Reduce conversion cache from Executable to KFunction.
+* #738: Fix JacksonInject priority.
+* #732: SequenceSerializer removed.
+* #727: Fixed overriding findCreatorAnnotation instead of hasCreatorAnnotation
+
+# 2.16.1
WrongWrong (@k163377)
* #733: Fix problem with Serializable objects not implementing readResolve
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 865897023..9a161f2c2 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -16,6 +16,138 @@ Co-maintainers:
=== Releases ===
------------------------------------------------------------------------
+2.20.0 (not yet released)
+#1020: Exceptions thrown by the old StrictNullChecks are now the similar to the new StrictNullChecks.
+ This means that the old StrictNullChecks will no longer throw MissingKotlinParameterException.
+ See PR for what is thrown and how error messages change.
+#1018: Improved handling of `value class` has improved performance for both serialization and deserialization.
+ In particular, for serialization, proper caching has improved throughput by a factor of 2 or more in the general cases.
+ Also, replacing function execution by reflection with `MethodHandle` improved throughput by several percent for both serialization and deserialization.
+ In cases where the number of properties of a `value class` in the processing target is large, there is a possibility to obtain a larger improvement.
+ Please note that this modification causes a destructive change in that exceptions thrown during deserialization of
+ `value class` are no longer wrapped in an `InvocationTargetException`.
+#969: Deprecated content has been cleaned up with the version upgrade.
+#967: Kotlin has been upgraded to 2.0.21.
+- Generate SBOMs [JSTEP-14]
+
+2.19.2 (18-Jul-2025)
+2.19.1 (13-Jun-2025)
+
+No changes since 2.19.0
+
+2.19.0 (24-Apr-2025)
+
+#959: Extension functions has been added to simplify `configOverride` calls to `ObjectMapper` and `Module.SetupContext`.
+#954: Replaced `OptBoolean` of internal caching with a common implementation.
+#944: Common util is now used for member accessibility overrides.
+#937: For `readValue` and other shorthands for `ObjectMapper` deserialization methods,
+ type consistency checks have been added.
+ A `RuntimeJsonMappingException` will be thrown in case of inconsistency.
+ This fixes a problem that broke `Kotlin` null safety by reading null as a value even if the type parameter was specified as non-null.
+ It also checks for custom errors in ObjectMapper that cause a different value to be read than the specified type parameter.
+#929: Added consideration of `JsonProperty.isRequired` added in `2.19` in `hasRequiredMarker` processing.
+ Previously `JsonProperty.required` was defined as `Boolean` with default `false`,
+ so `KotlinModule` was forced to override it if the value was `false`.
+ This made it impossible for users to override the parsed result by `KotlinModule`.
+ The new `JsonProperty.isRequired` is defined with three values, including the default,
+ so `KotlinModule` can now respect user specifications.
+#929: Fixed a problem with the `NullToEmptyCollection` and `NullToEmptyMap` options being applied to non-parameters
+ in the `hasRequiredMarker` process.
+ They currently do not work for setters or fields and are not related to serialization,
+ but were being incorrectly applied to their `required` decisions.
+#910: A default `KeyDeserializer` for `value class` has been added.
+ This eliminates the need to have a custom `KeyDeserializer` for each `value class` when using it as a key in a `Map`, if only simple boxing is needed.
+#889: Kotlin has been upgraded to 1.9.25.
+#885: A new `StrictNullChecks` option(KotlinFeature.NewStrictNullChecks) has been added which greatly improves throughput.
+ Benchmarks show a consistent throughput drop of less than 2% when enabled (prior to the improvement, the worst throughput drop was more than 30%).
+ Note that the new backend changes the exception thrown to `InvalidNullException` and with it the error message.
+ Also note that the base class for `MissingKotlinParameterException` was changed to `InvalidNullException` in #884.
+#884: The base class for `MissingKotlinParameterException` has been changed to `InvalidNullException`.
+ If you do not catch this exception or catch `MismatchedInputException`, the behavior is unchanged.
+ If you are catching both `MismatchedKotlinParameterException` and `InvalidNullException`, you must catch `MismatchedKotlinParameterException` first.
+#883: The deprecation level has been raised to error for the `MissingKotlinParameterException` secondary constructor.
+ This is a problematic process that has been marked as deprecated for a very long time and will be removed in 2.20 or later.
+#878: Fixed a problem where settings like `@JsonSetter(nulls = AS_EMPTY)` were not being applied when the input was `undefined`.
+#869: By using Enum.entries in the acquisition of KotlinFeature.defaults, the initialization load was reduced, albeit slightly.
+#858: Minor performance improvement of findDefaultCreator in edge cases.
+#839: Remove useKotlinPropertyNameForGetter and unify with kotlinPropertyNameAsImplicitName.
+#835: Remove old SingletonSupport class and unified with KotlinFeature.SingletonSupport.
+
+2.18.4 (06-May-2025)
+
+#923: Fixed a problem where the result of processing `hasRequiredMarker ` by a `KotlinModule` would also apply to
+ classes defined in `Java` when `NullToEmptyCollection` or `NullToEmptyMap` was enabled.
+#920: Minor refactorings were made that did not affect behavior.
+
+2.18.3 (28-Feb-2025)
+
+#904: Fixed a problem where context was not being propagated properly when serializing an unboxed value of `value class`
+ or a value retrieved with `JsonValue`.
+ This fixes a problem where an error would occur when serializing a `value class` that wraps a `Map`(#873).
+
+2.18.2 (27-Nov-2024)
+2.18.1 (28-Oct-2024)
+
+No changes since 2.18.0
+
+2.18.0 (26-Sep-2024)
+
+#818: The implementation of the search process for the `JsonCreator` (often the primary constructor)
+ used by default for deserialization has been changed to `AnnotationIntrospector#findDefaultCreator`.
+ This has improved first-time processing performance and memory usage.
+ It also solves the problem of `findCreatorAnnotation` results by `AnnotationIntrospector` registered by the user
+ being ignored depending on the order in which modules are registered.
+#817: The convertValue extension function now accepts null
+#803: Kotlin has been upgraded to 1.8.10.
+ The reason 1.8.22 is not used is to avoid KT-65156.
+#782: Content marked as deprecated has been reorganized.
+ Several constructors and accessors to properties of KotlinModule.Builder that were marked as DeprecationLevel.ERROR have been removed.
+ Also, the content marked as DeprecationLevel.WARNING is now DeprecationLevel.ERROR.
+#542: Remove meaningless checks and properties in KNAI.
+
+2.17.3 (01-Nov-2024)
+
+No changes since 2.17.2
+
+2.17.2 (05-Jul-2024)
+
+#799: Fixed problem with code compiled with 2.17.x losing backward compatibility.
+
+2.17.1 (04-May-2024)
+
+#776: Delete Duration conversion that was no longer needed.
+#779: Errors no longer occur when processing Record types defined in Java.
+
+2.17.0 (12-Mar-2024)
+
+#768: Added value class deserialization support.
+#760: Caching is now applied to the entire parameter parsing process on Kotlin.
+#758: Deprecated SingletonSupport and related properties to be consistent with KotlinFeature.SingletonSupport.
+#755: Changes in constructor invocation and argument management.
+ This change degrades performance in cases where the constructor is called without default arguments, but improves performance in other cases.
+#751: The KotlinModule#useKotlinPropertyNameForGetter property was deprecated because it differed from the name of the KotlinFeature.
+ Please use KotlinModule#kotlinPropertyNameAsImplicitName from now on.
+#747: Improved performance related to KotlinModule initialization and setupModule.
+ With this change, the KotlinModule initialization error when using Kotlin 1.4 or lower has been eliminated.
+#746: The KotlinModule#serialVersionUID is set to private.
+#745: Modified isKotlinClass determination method.
+#744: Functions that were already marked as deprecated,
+ such as the primary constructor in KotlinModule and some functions in Builder,
+ are scheduled for removal in 2.18 and their DeprecationLevel has been raised to Error.
+ Hidden constructors that were left in for compatibility are also marked for removal.
+ This PR also adds a hidden no-argument constructor to facilitate initialization from reflection.
+ See the PR for details.
+#743: The handling of deserialization using vararg arguments has been improved to allow deserialization even when the input to the vararg argument is undefined.
+ In addition, vararg arguments are now reported as non-required.
+#742: Minor performance improvements to NullToEmptyCollection/Map.
+#741: Changed to allow KotlinFeature to be set in the function that registers a KotlinModule.
+ The `jacksonObjectMapper {}` and `registerKotlinModule {}` lambdas allow configuration for KotlinModule.
+#740: Reduce conversion cache from Executable to KFunction.
+ This will reduce memory usage efficiency and total memory consumption, but may result in a minor performance degradation in use cases where a large number of factory functions are used as JsonCreator.
+#738: JacksonInject is now preferred over the default argument(fixes #722).
+#732: SequenceSerializer removed.
+#727: Fixed overriding findCreatorAnnotation instead of hasCreatorAnnotation.
+
2.16.2 (09-Mar-2024)
No changes since 2.16.1
diff --git a/src/main/java/com/fasterxml/jackson/module/kotlin/WrapsNullableValueClassDeserializer.java b/src/main/java/com/fasterxml/jackson/module/kotlin/WrapsNullableValueClassDeserializer.java
new file mode 100644
index 000000000..cfc6b0316
--- /dev/null
+++ b/src/main/java/com/fasterxml/jackson/module/kotlin/WrapsNullableValueClassDeserializer.java
@@ -0,0 +1,56 @@
+package com.fasterxml.jackson.module.kotlin;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import kotlin.jvm.JvmClassMappingKt;
+import kotlin.reflect.KClass;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+
+/**
+ * An interface to be inherited by JsonDeserializer that handles value classes that may wrap nullable.
+ */
+// To ensure maximum compatibility with StdDeserializer, this class is written in Java.
+public abstract class WrapsNullableValueClassDeserializer extends StdDeserializer {
+ protected WrapsNullableValueClassDeserializer(@NotNull KClass> vc) {
+ super(JvmClassMappingKt.getJavaClass(vc));
+ }
+
+ protected WrapsNullableValueClassDeserializer(@NotNull Class> vc) {
+ super(vc);
+ }
+
+ protected WrapsNullableValueClassDeserializer(@NotNull JavaType valueType) {
+ super(valueType);
+ }
+
+ protected WrapsNullableValueClassDeserializer(@NotNull StdDeserializer src) {
+ super(src);
+ }
+
+ @Override
+ @NotNull
+ public final Class handledType() {
+ //noinspection unchecked
+ return (Class) super.handledType();
+ }
+
+ /**
+ * If the parameter definition is a value class that wraps a nullable and is non-null,
+ * and the input to JSON is explicitly null, this value is used.
+ * Note that this will only be called from the KotlinValueInstantiator,
+ * so it will not work for top-level deserialization of value classes.
+ */
+ // It is defined so that null can also be returned so that Nulls.SKIP can be applied.
+ @Nullable
+ public abstract D getBoxedNullValue();
+
+ @Override
+ public abstract D deserialize(@NotNull JsonParser p, @NotNull DeserializationContext ctxt)
+ throws IOException, JacksonException;
+}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt
new file mode 100644
index 000000000..b7cdfc393
--- /dev/null
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt
@@ -0,0 +1,68 @@
+package com.fasterxml.jackson.module.kotlin
+
+import kotlin.reflect.KParameter
+
+internal class BucketGenerator private constructor(paramSize: Int, instanceParameter: KParameter?, instance: Any?) {
+ private val originalParameters = arrayOfNulls(paramSize)
+ private val originalArguments = arrayOfNulls(paramSize)
+ private val initialCount: Int
+
+ init {
+ if (instance != null) {
+ originalParameters[0] = instanceParameter
+ originalArguments[0] = instance
+ initialCount = 1
+ } else {
+ initialCount = 0
+ }
+ }
+
+ fun generate(): ArgumentBucket = ArgumentBucket(
+ parameters = originalParameters.clone(),
+ arguments = originalArguments.clone(),
+ count = initialCount
+ )
+
+ companion object {
+ fun forConstructor(paramSize: Int): BucketGenerator = BucketGenerator(paramSize, null, null)
+
+ fun forMethod(paramSize: Int, instanceParameter: KParameter, instance: Any): BucketGenerator =
+ BucketGenerator(paramSize, instanceParameter, instance)
+ }
+}
+
+internal class ArgumentBucket(
+ private val parameters: Array,
+ val arguments: Array,
+ private var count: Int
+) : Map {
+ operator fun set(key: KParameter, value: Any?) {
+ arguments[key.index] = value
+ parameters[key.index] = key
+
+ // Multiple calls are not checked because internally no calls are made more than once per argument.
+ count++
+ }
+
+ val isFullInitialized: Boolean get() = count == arguments.size
+
+ private class Entry(override val key: KParameter, override val value: Any?) : Map.Entry
+
+ override val entries: Set>
+ get() = parameters.mapNotNull { key -> key?.let { Entry(it, arguments[it.index]) } }.toSet()
+ override val keys: Set
+ get() = parameters.filterNotNull().toSet()
+ override val size: Int
+ get() = count
+ override val values: Collection
+ get() = keys.map { arguments[it.index] }
+
+ override fun isEmpty(): Boolean = this.size == 0
+
+ // Skip the check here, as it is only called after the check for containsKey.
+ override fun get(key: KParameter): Any? = arguments[key.index]
+
+ override fun containsValue(value: Any?): Boolean = keys.any { arguments[it.index] == value }
+
+ override fun containsKey(key: KParameter): Boolean = parameters.any { it?.index == key.index }
+}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorValueCreator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorValueCreator.kt
index 4f7116798..6047694af 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorValueCreator.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorValueCreator.kt
@@ -5,6 +5,7 @@ import kotlin.reflect.jvm.isAccessible
internal class ConstructorValueCreator(override val callable: KFunction) : ValueCreator() {
override val accessible: Boolean = callable.isAccessible
+ override val bucketGenerator: BucketGenerator = BucketGenerator.forConstructor(callable.parameters.size)
init {
// To prevent the call from failing, save the initial value and then rewrite the flag.
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
index c27f82c69..7902b072e 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Converters.kt
@@ -5,7 +5,12 @@ import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.StdConverter
-import kotlin.reflect.KClass
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
+import java.lang.reflect.Method
+import java.lang.reflect.Type
+import java.util.UUID
import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration
@@ -22,7 +27,7 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon
}
internal object KotlinDurationValueToJavaDurationConverter : StdConverter() {
- private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class) }
+ private val boxConverter by lazy { LongValueClassBoxConverter(KotlinDuration::class.java) }
override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value))
}
@@ -44,18 +49,163 @@ internal object JavaToKotlinDurationConverter : StdConverter(
- unboxedClass: Class,
- valueClass: KClass
-) : StdConverter() {
- private val boxMethod = valueClass.java.getDeclaredMethod("box-impl", unboxedClass).apply {
- if (!this.isAccessible) this.isAccessible = true
+internal sealed class ValueClassBoxConverter : StdConverter() {
+ abstract val boxedClass: Class
+ abstract val boxHandle: MethodHandle
+
+ protected fun rawBoxHandle(
+ unboxedClass: Class<*>,
+ ): MethodHandle = MethodHandles.lookup().findStatic(
+ boxedClass,
+ "box-impl",
+ MethodType.methodType(boxedClass, unboxedClass),
+ )
+
+ val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
+
+ companion object {
+ fun create(
+ unboxedClass: Class<*>,
+ valueClass: Class<*>,
+ ): ValueClassBoxConverter<*, *> = when (unboxedClass) {
+ Int::class.java -> IntValueClassBoxConverter(valueClass)
+ Long::class.java -> LongValueClassBoxConverter(valueClass)
+ String::class.java -> StringValueClassBoxConverter(valueClass)
+ UUID::class.java -> JavaUuidValueClassBoxConverter(valueClass)
+ else -> GenericValueClassBoxConverter(unboxedClass, valueClass)
+ }
}
+ // If the wrapped type is explicitly specified, it is inherited for the sake of distinction
+ internal sealed class Specified : ValueClassBoxConverter()
+}
+
+// region: Converters for common classes as wrapped values, add as needed.
+internal class IntValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(Int::class.java).asType(INT_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: Int): D = boxHandle.invokeExact(value) as D
+}
+
+internal class LongValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(Long::class.java).asType(LONG_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: Long): D = boxHandle.invokeExact(value) as D
+}
+
+internal class StringValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(String::class.java).asType(STRING_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: String?): D = boxHandle.invokeExact(value) as D
+}
+
+internal class JavaUuidValueClassBoxConverter(
+ override val boxedClass: Class,
+) : ValueClassBoxConverter.Specified() {
+ override val boxHandle: MethodHandle = rawBoxHandle(UUID::class.java).asType(JAVA_UUID_TO_ANY_METHOD_TYPE)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun convert(value: UUID?): D = boxHandle.invokeExact(value) as D
+}
+// endregion
+
+/**
+ * A converter that only performs box processing for the value class.
+ * Note that constructor-impl is not called.
+ * @param S is nullable because value corresponds to a nullable value class.
+ * see [io.github.projectmapk.jackson.module.kogera.annotationIntrospector.KotlinFallbackAnnotationIntrospector.findNullSerializer]
+ */
+internal class GenericValueClassBoxConverter(
+ unboxedClass: Class,
+ override val boxedClass: Class,
+) : ValueClassBoxConverter() {
+ override val boxHandle: MethodHandle = rawBoxHandle(unboxedClass).asType(ANY_TO_ANY_METHOD_TYPE)
+
@Suppress("UNCHECKED_CAST")
- override fun convert(value: S): D = boxMethod.invoke(null, value) as D
+ override fun convert(value: S): D = boxHandle.invokeExact(value) as D
+}
+
+internal sealed class ValueClassUnboxConverter : StdConverter() {
+ abstract val valueClass: Class
+ abstract val unboxedType: Type
+ abstract val unboxHandle: MethodHandle
+
+ final override fun getInputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(valueClass)
+ final override fun getOutputType(typeFactory: TypeFactory): JavaType = typeFactory.constructType(unboxedType)
val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
+
+ companion object {
+ fun create(valueClass: Class<*>): ValueClassUnboxConverter<*, *> {
+ val unboxMethod = valueClass.getDeclaredMethod("unbox-impl")
+ val unboxedType = unboxMethod.genericReturnType
+
+ return when (unboxedType) {
+ Int::class.java -> IntValueClassUnboxConverter(valueClass, unboxMethod)
+ Long::class.java -> LongValueClassUnboxConverter(valueClass, unboxMethod)
+ String::class.java -> StringValueClassUnboxConverter(valueClass, unboxMethod)
+ UUID::class.java -> JavaUuidValueClassUnboxConverter(valueClass, unboxMethod)
+ else -> GenericValueClassUnboxConverter(valueClass, unboxedType, unboxMethod)
+ }
+ }
+ }
+}
+
+internal class IntValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = Int::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_INT_METHOD_TYPE)
+
+ override fun convert(value: T): Int = unboxHandle.invokeExact(value) as Int
+}
+
+internal class LongValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = Long::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_LONG_METHOD_TYPE)
+
+ override fun convert(value: T): Long = unboxHandle.invokeExact(value) as Long
+}
+
+internal class StringValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = String::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_STRING_METHOD_TYPE)
+
+ override fun convert(value: T): String? = unboxHandle.invokeExact(value) as String?
+}
+
+internal class JavaUuidValueClassUnboxConverter(
+ override val valueClass: Class,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxedType: Type get() = UUID::class.java
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_JAVA_UUID_METHOD_TYPE)
+
+ override fun convert(value: T): UUID? = unboxHandle.invokeExact(value) as UUID?
+}
+
+internal class GenericValueClassUnboxConverter(
+ override val valueClass: Class,
+ override val unboxedType: Type,
+ unboxMethod: Method,
+) : ValueClassUnboxConverter() {
+ override val unboxHandle: MethodHandle = unreflectAsType(unboxMethod, ANY_TO_ANY_METHOD_TYPE)
+
+ override fun convert(value: T): Any? = unboxHandle.invokeExact(value)
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt
index add73669b..fbd70c401 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Exceptions.kt
@@ -2,8 +2,7 @@ package com.fasterxml.jackson.module.kotlin
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.JsonMappingException
-import com.fasterxml.jackson.databind.exc.MismatchedInputException
-import java.io.Closeable
+import com.fasterxml.jackson.databind.exc.InvalidNullException
import kotlin.reflect.KParameter
/**
@@ -11,12 +10,12 @@ import kotlin.reflect.KParameter
* parameter was missing or null.
*/
@Deprecated(
- "It is recommended that MismatchedInputException be referenced when possible," +
- " as the change is discussed for 2.17 and later." +
+ "It is recommended that InvalidNullException be referenced when possible," +
+ " as the change is discussed for 2.20 and later." +
" See #617 for details.",
ReplaceWith(
- "MismatchedInputException",
- "com.fasterxml.jackson.databind.exc.MismatchedInputException"
+ "InvalidNullException",
+ "com.fasterxml.jackson.databind.exc.InvalidNullException"
),
DeprecationLevel.WARNING
)
@@ -24,18 +23,11 @@ import kotlin.reflect.KParameter
// This is a temporary workaround for #572 and we will eventually remove this class.
class MissingKotlinParameterException(
@property:Deprecated(
- "KParameter is not serializable and will be removed in 2.17 or later. See #572 for details.",
+ "KParameter is not serializable and will be removed in 2.20 or later. See #572 for details.",
level = DeprecationLevel.WARNING
)
@Transient
val parameter: KParameter,
processor: JsonParser? = null,
msg: String
-) : MismatchedInputException(processor, msg) {
- @Deprecated("Use main constructor", ReplaceWith("MissingKotlinParameterException(KParameter, JsonParser?, String)"))
- constructor(
- parameter: KParameter,
- processor: Closeable? = null,
- msg: String
- ) : this(parameter, processor as JsonParser, msg)
-}
+) : InvalidNullException(processor, msg, null)
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt
index 7ccf411c1..dc75e7b26 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Extensions.kt
@@ -4,12 +4,14 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.TreeNode
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.MappingIterator
+import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ObjectReader
+import com.fasterxml.jackson.databind.RuntimeJsonMappingException
+import com.fasterxml.jackson.databind.cfg.MutableConfigOverride
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ArrayNode
@@ -20,7 +22,6 @@ import java.io.Reader
import java.math.BigDecimal
import java.math.BigInteger
import java.net.URL
-import java.util.*
import kotlin.reflect.KClass
fun kotlinModule(initializer: KotlinModule.Builder.() -> Unit = {}): KotlinModule {
@@ -35,28 +36,147 @@ fun jsonMapper(initializer: JsonMapper.Builder.() -> Unit = {}): JsonMapper {
return builder.build()
}
+// region: Do not remove the default argument for functions that take a builder as an argument for compatibility.
+// The default argument can be removed in 2.21 or later. See #775 for the history.
fun jacksonObjectMapper(): ObjectMapper = jsonMapper { addModule(kotlinModule()) }
+fun jacksonObjectMapper(initializer: KotlinModule.Builder.() -> Unit = {}): ObjectMapper =
+ jsonMapper { addModule(kotlinModule(initializer)) }
+
fun jacksonMapperBuilder(): JsonMapper.Builder = JsonMapper.builder().addModule(kotlinModule())
+fun jacksonMapperBuilder(initializer: KotlinModule.Builder.() -> Unit = {}): JsonMapper.Builder =
+ JsonMapper.builder().addModule(kotlinModule(initializer))
fun ObjectMapper.registerKotlinModule(): ObjectMapper = this.registerModule(kotlinModule())
+fun ObjectMapper.registerKotlinModule(initializer: KotlinModule.Builder.() -> Unit = {}): ObjectMapper =
+ this.registerModule(kotlinModule(initializer))
+// endregion
inline fun jacksonTypeRef(): TypeReference = object: TypeReference() {}
+@PublishedApi
+internal inline fun Any?.checkTypeMismatch(): T {
+ // Basically, this check assumes that T is non-null and the value is null.
+ // Since this can be caused by both input or ObjectMapper implementation errors,
+ // a more abstract RuntimeJsonMappingException is thrown.
+ if (this !is T) {
+ val nullability = if (null is T) "?" else "(non-null)"
+
+ // Since the databind implementation of MappingIterator throws RuntimeJsonMappingException,
+ // JsonMappingException was not used to unify the behavior.
+ throw RuntimeJsonMappingException(
+ "Deserialized value did not match the specified type; " +
+ "specified ${T::class.qualifiedName}${nullability} but was ${this?.let { it::class.qualifiedName }}"
+ )
+ }
+ return this
+}
+
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
inline fun ObjectMapper.readValue(jp: JsonParser): T = readValue(jp, jacksonTypeRef())
-inline fun ObjectMapper.readValues(jp: JsonParser): MappingIterator = readValues(jp, jacksonTypeRef())
+ .checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.readValues].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
+inline fun ObjectMapper.readValues(jp: JsonParser): MappingIterator {
+ val values = readValues(jp, jacksonTypeRef())
+
+ return object : MappingIterator(values) {
+ override fun nextValue(): T = super.nextValue().checkTypeMismatch()
+ }
+}
-inline fun ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef())
-inline fun ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef())
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
+inline fun ObjectMapper.readValue(src: File): T = readValue(src, jacksonTypeRef()).checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
+inline fun ObjectMapper.readValue(src: URL): T = readValue(src, jacksonTypeRef()).checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
inline fun ObjectMapper.readValue(content: String): T = readValue(content, jacksonTypeRef())
-inline fun ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef())
+ .checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
+inline fun ObjectMapper.readValue(src: Reader): T = readValue(src, jacksonTypeRef()).checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
inline fun ObjectMapper.readValue(src: InputStream): T = readValue(src, jacksonTypeRef())
+ .checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
inline fun ObjectMapper.readValue(src: ByteArray): T = readValue(src, jacksonTypeRef())
-
+ .checkTypeMismatch()
+
+/**
+ * Shorthand for [ObjectMapper.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
inline fun ObjectMapper.treeToValue(n: TreeNode): T = readValue(this.treeAsTokens(n), jacksonTypeRef())
-inline fun ObjectMapper.convertValue(from: Any): T = convertValue(from, jacksonTypeRef())
-
+ .checkTypeMismatch()
+/**
+ * Shorthand for [ObjectMapper.convertValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectMapper].
+ */
+inline fun ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef())
+ .checkTypeMismatch()
+
+/**
+ * Shorthand for [ObjectReader.readValue].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectReader].
+ */
inline fun ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef())
-inline fun ObjectReader.readValuesTyped(jp: JsonParser): Iterator = readValues(jp, jacksonTypeRef())
+ .checkTypeMismatch()
+/**
+ * Shorthand for [ObjectReader.readValues].
+ * @throws RuntimeJsonMappingException Especially if [T] is non-null and the value read is null.
+ * Other cases where the read value is of a different type than [T]
+ * due to an incorrect customization to [ObjectReader].
+ */
+inline fun ObjectReader.readValuesTyped(jp: JsonParser): Iterator {
+ val values = readValues(jp, jacksonTypeRef())
+
+ return object : Iterator by values {
+ override fun next(): T = values.next().checkTypeMismatch()
+ }
+}
inline fun ObjectReader.treeToValue(n: TreeNode): T? = readValue(this.treeAsTokens(n), jacksonTypeRef())
inline fun ObjectMapper.addMixIn(): ObjectMapper = this.addMixIn(T::class.java, U::class.java)
@@ -97,9 +217,6 @@ operator fun ObjectNode.minusAssign(fields: Collection) = Unit.apply { r
operator fun JsonNode.contains(field: String) = has(field)
operator fun JsonNode.contains(index: Int) = has(index)
-internal fun JsonMappingException.wrapWithPath(refFrom: Any?, refFieldName: String) = JsonMappingException.wrapWithPath(this, refFrom, refFieldName)
-internal fun JsonMappingException.wrapWithPath(refFrom: Any?, index: Int) = JsonMappingException.wrapWithPath(this, refFrom, index)
-
fun SimpleModule.addSerializer(kClass: KClass, serializer: JsonSerializer): SimpleModule = this.apply {
kClass.javaPrimitiveType?.let { addSerializer(it, serializer) }
addSerializer(kClass.javaObjectType, serializer)
@@ -110,22 +227,6 @@ fun SimpleModule.addDeserializer(kClass: KClass, deserializer: Json
addDeserializer(kClass.javaObjectType, deserializer)
}
-internal fun Int.toBitSet(): BitSet {
- var i = this
- var index = 0
- val bits = BitSet(32)
- while (i != 0) {
- if (i % 2 != 0) {
- bits.set(index)
- }
- ++index
- i = i shr 1
- }
- return bits
-}
-
-// In the future, value classes without @JvmInline will be available, and unboxing may not be able to handle it.
-// https://github.com/FasterXML/jackson-module-kotlin/issues/464
-// The JvmInline annotation can be added to Java classes,
-// so the isKotlinClass decision is necessary (the order is preferable in terms of possible frequency).
-internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } && this.isKotlinClass()
+inline fun ObjectMapper.configOverride(): MutableConfigOverride = configOverride(T::class.java)
+inline fun Module.SetupContext.configOverride(): MutableConfigOverride =
+ configOverride(T::class.java)
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt
new file mode 100644
index 000000000..5d49ef118
--- /dev/null
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/InternalCommons.kt
@@ -0,0 +1,65 @@
+package com.fasterxml.jackson.module.kotlin
+
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.databind.JsonMappingException
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
+import java.lang.reflect.AnnotatedElement
+import java.lang.reflect.Method
+import java.util.*
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.jvm.javaField
+import kotlin.reflect.jvm.jvmErasure
+
+internal val defaultConstructorMarker: Class<*> by lazy {
+ Class.forName("kotlin.jvm.internal.DefaultConstructorMarker")
+}
+
+internal fun JsonMappingException.wrapWithPath(refFrom: Any?, refFieldName: String) = JsonMappingException.wrapWithPath(this, refFrom, refFieldName)
+internal fun JsonMappingException.wrapWithPath(refFrom: Any?, index: Int) = JsonMappingException.wrapWithPath(this, refFrom, index)
+
+internal fun Int.toBitSet(): BitSet {
+ var i = this
+ var index = 0
+ val bits = BitSet(32)
+ while (i != 0) {
+ if (i % 2 != 0) {
+ bits.set(index)
+ }
+ ++index
+ i = i shr 1
+ }
+ return bits
+}
+
+// In the future, value classes without @JvmInline will be available, and unboxing may not be able to handle it.
+// https://github.com/FasterXML/jackson-module-kotlin/issues/464
+// The JvmInline annotation can be added to Java classes,
+// so the isKotlinClass decision is necessary (the order is preferable in terms of possible frequency).
+internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline } && this.isKotlinClass()
+
+internal fun KType.erasedType(): Class = this.jvmErasure.java
+
+internal fun AnnotatedElement.hasCreatorAnnotation(): Boolean = getAnnotation(JsonCreator::class.java)
+ ?.let { it.mode != JsonCreator.Mode.DISABLED }
+ ?: false
+
+// Determine if the unbox result of value class is nullable
+internal fun KClass<*>.wrapsNullable(): Boolean =
+ this.memberProperties.first { it.javaField != null }.returnType.isMarkedNullable
+
+internal val ANY_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Any::class.java) }
+internal val ANY_TO_INT_METHOD_TYPE by lazy {MethodType.methodType(Int::class.java, Any::class.java) }
+internal val ANY_TO_LONG_METHOD_TYPE by lazy {MethodType.methodType(Long::class.java, Any::class.java) }
+internal val ANY_TO_STRING_METHOD_TYPE by lazy {MethodType.methodType(String::class.java, Any::class.java) }
+internal val ANY_TO_JAVA_UUID_METHOD_TYPE by lazy {MethodType.methodType(UUID::class.java, Any::class.java) }
+internal val INT_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Int::class.java) }
+internal val LONG_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, Long::class.java) }
+internal val STRING_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, String::class.java) }
+internal val JAVA_UUID_TO_ANY_METHOD_TYPE by lazy {MethodType.methodType(Any::class.java, UUID::class.java) }
+
+internal fun unreflect(method: Method): MethodHandle = MethodHandles.lookup().unreflect(method)
+internal fun unreflectAsType(method: Method, type: MethodType): MethodHandle = unreflect(method).asType(type)
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt
index a06bd0142..92533d559 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinAnnotationIntrospector.kt
@@ -1,31 +1,30 @@
package com.fasterxml.jackson.module.kotlin
-import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.OptBoolean
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.Module
-import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.*
import com.fasterxml.jackson.databind.jsontype.NamedType
import com.fasterxml.jackson.databind.util.Converter
import java.lang.reflect.AccessibleObject
-import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method
-import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty1
+import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
-import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.valueParameters
-import kotlin.reflect.jvm.*
+import kotlin.reflect.jvm.javaGetter
+import kotlin.reflect.jvm.javaSetter
+import kotlin.reflect.jvm.javaType
+import kotlin.reflect.jvm.kotlinProperty
import kotlin.time.Duration
-
internal class KotlinAnnotationIntrospector(
private val context: Module.SetupContext,
private val cache: ReflectionCache,
@@ -38,40 +37,42 @@ internal class KotlinAnnotationIntrospector(
// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it
// this likely impacts this class to be accurate about what COULD be considered required
- override fun hasRequiredMarker(m: AnnotatedMember): Boolean? {
- val hasRequired = cache.javaMemberIsRequired(m) {
+ // If a new isRequired is explicitly specified or the old required is true, those values take precedence.
+ // In other cases, override is done by KotlinModule.
+ private fun JsonProperty.forceRequiredByAnnotation(): Boolean? = when {
+ isRequired != OptBoolean.DEFAULT -> isRequired.asBoolean()
+ required -> true
+ else -> null
+ }
+
+ private fun AccessibleObject.forceRequiredByAnnotation(): Boolean? =
+ getAnnotation(JsonProperty::class.java)?.forceRequiredByAnnotation()
+
+ override fun hasRequiredMarker(
+ m: AnnotatedMember
+ ): Boolean? = m.takeIf { it.member.declaringClass.isKotlinClass() }?.let { _ ->
+ cache.javaMemberIsRequired(m) {
try {
- when {
- nullToEmptyCollection && m.type.isCollectionLikeType -> false
- nullToEmptyMap && m.type.isMapLikeType -> false
- m.member.declaringClass.isKotlinClass() -> when (m) {
- is AnnotatedField -> m.hasRequiredMarker()
- is AnnotatedMethod -> m.hasRequiredMarker()
- is AnnotatedParameter -> m.hasRequiredMarker()
- else -> null
- }
+ when (m) {
+ is AnnotatedField -> m.hasRequiredMarker()
+ is AnnotatedMethod -> m.hasRequiredMarker()
+ is AnnotatedParameter -> m.hasRequiredMarker()
else -> null
}
- } catch (ex: UnsupportedOperationException) {
+ } catch (_: UnsupportedOperationException) {
null
}
}
- return hasRequired
- }
-
- override fun findCreatorAnnotation(config: MapperConfig<*>, a: Annotated): JsonCreator.Mode? {
-
- // TODO: possible work around for JsonValue class that requires the class constructor to have the JsonCreator(Mode.DELEGATED) set?
- // since we infer the creator at times for these methods, the wrong mode could be implied.
-
- // findCreatorBinding used to be a clearer way to set this, but we need to set the mode here to disambugiate the intent of the constructor
- return super.findCreatorAnnotation(config, a)
}
override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
is AnnotatedMethod -> a.findValueClassReturnType()?.let {
+ // To make annotations that process JavaDuration work,
+ // it is necessary to set up the conversion to JavaDuration here.
+ // This conversion will cause the deserialization settings for KotlinDuration to be ignored.
if (useJavaDurationConversion && it == Duration::class) {
+ // For early return, the same process is placed as the branch regarding AnnotatedClass.
if (a.rawReturnType == Duration::class.java)
KotlinToJavaDurationConverter
else
@@ -90,38 +91,13 @@ internal class KotlinAnnotationIntrospector(
else -> null
}
- // Determine if the unbox result of value class is nullAable
- // @see findNullSerializer
- private fun KClass<*>.requireRebox(): Boolean =
- this.memberProperties.first { it.javaField != null }.returnType.isMarkedNullable
-
// Perform proper serialization even if the value wrapped by the value class is null.
// If value is a non-null object type, it must not be reboxing.
override fun findNullSerializer(am: Annotated): JsonSerializer<*>? = (am as? AnnotatedMethod)
?.findValueClassReturnType()
- ?.takeIf { it.requireRebox() }
+ ?.takeIf { it.wrapsNullable() }
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
- override fun findDeserializationConverter(a: Annotated): Any? {
- if (!useJavaDurationConversion) return null
-
- return (a as? AnnotatedParameter)?.let { param ->
- @Suppress("UNCHECKED_CAST")
- val function: KFunction<*> = when (val owner = param.owner.member) {
- is Constructor<*> -> cache.kotlinFromJava(owner as Constructor)
- is Method -> cache.kotlinFromJava(owner)
- else -> null
- } ?: return@let null
- val valueParameter = function.valueParameters[a.index]
-
- if (valueParameter.type.classifier == Duration::class) {
- JavaToKotlinDurationConverter
- } else {
- null
- }
- }
- }
-
/**
* Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
* at compile-time to Kotlin. This makes [com.fasterxml.jackson.annotation.JsonSubTypes] redundant.
@@ -136,28 +112,9 @@ internal class KotlinAnnotationIntrospector(
}
private fun AnnotatedField.hasRequiredMarker(): Boolean? {
- val byAnnotation = (member as Field).isRequiredByAnnotation()
- val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()
-
- return requiredAnnotationOrNullability(byAnnotation, byNullability)
- }
-
- private fun AccessibleObject.isRequiredByAnnotation(): Boolean? = annotations
- ?.firstOrNull { it.annotationClass == JsonProperty::class }
- ?.let { it as JsonProperty }
- ?.required
-
- private fun requiredAnnotationOrNullability(byAnnotation: Boolean?, byNullability: Boolean?): Boolean? {
- if (byAnnotation != null && byNullability != null) {
- return byAnnotation || byNullability
- } else if (byNullability != null) {
- return byNullability
- }
- return byAnnotation
- }
-
- private fun Method.isRequiredByAnnotation(): Boolean? {
- return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
+ val field = member as Field
+ return field.forceRequiredByAnnotation()
+ ?: field.kotlinProperty?.returnType?.isRequired()
}
// Since Kotlin's property has the same Type for each field, getter, and setter,
@@ -172,20 +129,19 @@ internal class KotlinAnnotationIntrospector(
private fun AnnotatedMethod.getRequiredMarkerFromCorrespondingAccessor(): Boolean? {
member.declaringClass.kotlin.declaredMemberProperties.forEach { kProperty ->
if (kProperty.javaGetter == this.member || (kProperty as? KMutableProperty1)?.javaSetter == this.member) {
- val byAnnotation = this.member.isRequiredByAnnotation()
- val byNullability = kProperty.isRequiredByNullability()
- return requiredAnnotationOrNullability(byAnnotation, byNullability)
+ return member.forceRequiredByAnnotation() ?: kProperty.isRequiredByNullability()
}
}
return null
}
// Is the member method a regular method of the data class or
- private fun Method.getRequiredMarkerFromAccessorLikeMethod(): Boolean? = this.kotlinFunction?.let { method ->
- val byAnnotation = this.isRequiredByAnnotation()
- return when {
- method.isGetterLike() -> requiredAnnotationOrNullability(byAnnotation, method.returnType.isRequired())
- method.isSetterLike() -> requiredAnnotationOrNullability(byAnnotation, method.isMethodParameterRequired(0))
+ private fun Method.getRequiredMarkerFromAccessorLikeMethod(): Boolean? = cache.kotlinFromJava(this)?.let { func ->
+ forceRequiredByAnnotation() ?: when {
+ func.isGetterLike() -> func.returnType.isRequired()
+ // If nullToEmpty could be supported for setters,
+ // a branch similar to AnnotatedParameter.hasRequiredMarker should be added.
+ func.isSetterLike() -> func.valueParameters[0].isRequired()
else -> null
}
}
@@ -193,39 +149,26 @@ internal class KotlinAnnotationIntrospector(
private fun KFunction<*>.isGetterLike(): Boolean = parameters.size == 1
private fun KFunction<*>.isSetterLike(): Boolean = parameters.size == 2 && returnType == UNIT_TYPE
- private fun AnnotatedParameter.hasRequiredMarker(): Boolean? {
- val member = this.member
- val byAnnotation = this.getAnnotation(JsonProperty::class.java)?.required
-
- val byNullability = when (member) {
- is Constructor<*> -> member.kotlinFunction?.isConstructorParameterRequired(index)
- is Method -> member.kotlinFunction?.isMethodParameterRequired(index)
- else -> null
+ private fun AnnotatedParameter.hasRequiredMarker(): Boolean? = getAnnotation(JsonProperty::class.java)
+ ?.forceRequiredByAnnotation()
+ ?: run {
+ when {
+ nullToEmptyCollection && type.isCollectionLikeType -> false
+ nullToEmptyMap && type.isMapLikeType -> false
+ else -> cache.findKotlinParameter(this)?.isRequired()
+ }
}
- return requiredAnnotationOrNullability(byAnnotation, byNullability)
- }
-
private fun AnnotatedMethod.findValueClassReturnType() = cache.findValueClassReturnType(this)
- private fun KFunction<*>.isConstructorParameterRequired(index: Int): Boolean {
- return isParameterRequired(index)
- }
-
- private fun KFunction<*>.isMethodParameterRequired(index: Int): Boolean {
- return isParameterRequired(index + 1)
- }
-
- private fun KFunction<*>.isParameterRequired(index: Int): Boolean {
- val param = parameters[index]
- val paramType = param.type
- val javaType = paramType.javaType
- val isPrimitive = when (javaType) {
+ private fun KParameter.isRequired(): Boolean {
+ val paramType = type
+ val isPrimitive = when (val javaType = paramType.javaType) {
is Class<*> -> javaType.isPrimitive
else -> false
}
- return !paramType.isMarkedNullable && !param.isOptional &&
+ return !paramType.isMarkedNullable && !isOptional && !isVararg &&
!(isPrimitive && !context.isEnabled(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES))
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt
index acc665918..53915cc50 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinBeanDeserializerModifier.kt
@@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
// [module-kotlin#225]: keep Kotlin singletons as singletons
object KotlinBeanDeserializerModifier : BeanDeserializerModifier() {
+ private fun readResolve(): Any = KotlinBeanDeserializerModifier
+
override fun modifyDeserializer(
config: DeserializationConfig,
beanDesc: BeanDescription,
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt
index ce8bcf4f3..9ece03809 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt
@@ -10,6 +10,14 @@ import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.reflect.Method
+import java.lang.reflect.Modifier
+import java.util.UUID
+import kotlin.reflect.full.primaryConstructor
+import kotlin.reflect.jvm.javaMethod
import kotlin.time.Duration as KotlinDuration
object SequenceDeserializer : StdDeserializer>(Sequence::class.java) {
@@ -94,7 +102,172 @@ object ULongDeserializer : StdDeserializer(ULong::class.java) {
)
}
+// If the creator does not perform type conversion, implement a unique deserializer for each for fast invocation.
+internal sealed class NoConversionCreatorBoxDeserializer(
+ creator: Method,
+ converter: ValueClassBoxConverter,
+) : WrapsNullableValueClassDeserializer(converter.boxedClass) {
+ protected abstract val inputType: Class<*>
+ protected val handle: MethodHandle = MethodHandles
+ .filterReturnValue(unreflect(creator), converter.boxHandle)
+
+ // Since the input to handle must be strict, invoke should be implemented in each class
+ protected abstract fun invokeExact(value: S): D
+
+ // Cache the result of wrapping null, since the result is always expected to be the same.
+ @get:JvmName("boxedNullValue")
+ private val boxedNullValue: D by lazy {
+ // For the sake of commonality, it is unavoidably called without checking.
+ // It is controlled by KotlinValueInstantiator, so it is not expected to reach this branch.
+ @Suppress("UNCHECKED_CAST")
+ invokeExact(null as S)
+ }
+
+ final override fun getBoxedNullValue(): D = boxedNullValue
+
+ final override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D {
+ @Suppress("UNCHECKED_CAST")
+ return invokeExact(p.readValueAs(inputType) as S)
+ }
+
+ internal class WrapsInt(
+ creator: Method,
+ converter: IntValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = Int::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Int): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsLong(
+ creator: Method,
+ converter: LongValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = Long::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Long): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsString(
+ creator: Method,
+ converter: StringValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = String::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: String?): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsJavaUuid(
+ creator: Method,
+ converter: JavaUuidValueClassBoxConverter,
+ ) : NoConversionCreatorBoxDeserializer(creator, converter) {
+ override val inputType get() = UUID::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D
+ }
+
+ companion object {
+ fun create(creator: Method, converter: ValueClassBoxConverter.Specified) = when (converter) {
+ is IntValueClassBoxConverter -> WrapsInt(creator, converter)
+ is LongValueClassBoxConverter -> WrapsLong(creator, converter)
+ is StringValueClassBoxConverter -> WrapsString(creator, converter)
+ is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(creator, converter)
+ }
+ }
+}
+
+// Even if the creator performs type conversion, it is distinguished
+// because a speedup due to rtype matching of filterReturnValue can be expected for the specified type.
+internal class HasConversionCreatorWrapsSpecifiedBoxDeserializer(
+ creator: Method,
+ private val inputType: Class<*>,
+ converter: ValueClassBoxConverter,
+) : WrapsNullableValueClassDeserializer(converter.boxedClass) {
+ private val handle: MethodHandle
+
+ init {
+ val unreflect = unreflect(creator).run {
+ asType(type().changeParameterType(0, Any::class.java))
+ }
+ handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle)
+ }
+
+ // Cache the result of wrapping null, since the result is always expected to be the same.
+ @get:JvmName("boxedNullValue")
+ private val boxedNullValue: D by lazy { instantiate(null) }
+
+ override fun getBoxedNullValue(): D = boxedNullValue
+
+ // To instantiate the value class in the same way as other classes,
+ // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order.
+ // Input is null only when called from KotlinValueInstantiator.
+ @Suppress("UNCHECKED_CAST")
+ private fun instantiate(input: Any?): D = handle.invokeExact(input) as D
+
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D {
+ val input = p.readValueAs(inputType)
+ return instantiate(input)
+ }
+}
+
+internal class WrapsAnyValueClassBoxDeserializer(
+ creator: Method,
+ private val inputType: Class<*>,
+ converter: GenericValueClassBoxConverter,
+) : WrapsNullableValueClassDeserializer(converter.boxedClass) {
+ private val handle: MethodHandle
+
+ init {
+ val unreflect = unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE)
+ handle = MethodHandles.filterReturnValue(unreflect, converter.boxHandle)
+ }
+
+ // Cache the result of wrapping null, since the result is always expected to be the same.
+ @get:JvmName("boxedNullValue")
+ private val boxedNullValue: D by lazy { instantiate(null) }
+
+ override fun getBoxedNullValue(): D = boxedNullValue
+
+ // To instantiate the value class in the same way as other classes,
+ // it is necessary to call creator(e.g. constructor-impl) -> box-impl in that order.
+ // Input is null only when called from KotlinValueInstantiator.
+ @Suppress("UNCHECKED_CAST")
+ private fun instantiate(input: Any?): D = handle.invokeExact(input) as D
+
+ override fun deserialize(p: JsonParser, ctxt: DeserializationContext): D {
+ val input = p.readValueAs(inputType)
+ return instantiate(input)
+ }
+}
+
+private fun invalidCreatorMessage(m: Method): String =
+ "The argument size of a Creator that does not return a value class on the JVM must be 1, " +
+ "please fix it or use JsonDeserializer.\n" +
+ "Detected: ${m.parameters.joinToString(prefix = "${m.name}(", separator = ", ", postfix = ")") { it.name }}"
+
+private fun findValueCreator(type: JavaType, clazz: Class<*>): Method? {
+ clazz.declaredMethods.forEach { method ->
+ if (Modifier.isStatic(method.modifiers) && method.hasCreatorAnnotation()) {
+ // Do nothing if a correctly functioning Creator is defined
+ return method.takeIf { clazz != method.returnType }?.apply {
+ // Creator with an argument size not equal to 1 is currently not supported.
+ if (parameterCount != 1) {
+ throw InvalidDefinitionException.from(null as JsonParser?, invalidCreatorMessage(method), type)
+ }
+ }
+ }
+ }
+
+ // primaryConstructor.javaMethod for the value class returns constructor-impl
+ return clazz.kotlin.primaryConstructor?.javaMethod
+}
+
internal class KotlinDeserializers(
+ private val cache: ReflectionCache,
private val useJavaDurationConversion: Boolean,
) : Deserializers.Base() {
override fun findBeanDeserializer(
@@ -102,15 +275,35 @@ internal class KotlinDeserializers(
config: DeserializationConfig?,
beanDesc: BeanDescription?,
): JsonDeserializer<*>? {
+ val rawClass = type.rawClass
+
return when {
- type.isInterface && type.rawClass == Sequence::class.java -> SequenceDeserializer
- type.rawClass == Regex::class.java -> RegexDeserializer
- type.rawClass == UByte::class.java -> UByteDeserializer
- type.rawClass == UShort::class.java -> UShortDeserializer
- type.rawClass == UInt::class.java -> UIntDeserializer
- type.rawClass == ULong::class.java -> ULongDeserializer
- type.rawClass == KotlinDuration::class.java ->
+ type.isInterface && rawClass == Sequence::class.java -> SequenceDeserializer
+ rawClass == Regex::class.java -> RegexDeserializer
+ rawClass == UByte::class.java -> UByteDeserializer
+ rawClass == UShort::class.java -> UShortDeserializer
+ rawClass == UInt::class.java -> UIntDeserializer
+ rawClass == ULong::class.java -> ULongDeserializer
+ rawClass == KotlinDuration::class.java ->
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
+ rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass)?.let {
+ val unboxedClass = it.returnType
+ val converter = cache.getValueClassBoxConverter(unboxedClass, rawClass)
+
+ when (converter) {
+ is ValueClassBoxConverter.Specified -> {
+ val inputType = it.parameterTypes[0]
+
+ if (inputType == unboxedClass) {
+ NoConversionCreatorBoxDeserializer.create(it, converter)
+ } else {
+ HasConversionCreatorWrapsSpecifiedBoxDeserializer(it, inputType, converter)
+ }
+ }
+ is GenericValueClassBoxConverter ->
+ WrapsAnyValueClassBoxDeserializer(it, it.parameterTypes[0], converter)
+ }
+ }
else -> null
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt
index 93dba381f..1e9a62a99 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt
@@ -1,11 +1,13 @@
package com.fasterxml.jackson.module.kotlin
+import com.fasterxml.jackson.annotation.JsonSetter
+import com.fasterxml.jackson.databind.exc.InvalidNullException
import java.util.BitSet
/**
* @see KotlinModule.Builder
*/
-enum class KotlinFeature(private val enabledByDefault: Boolean) {
+enum class KotlinFeature(internal val enabledByDefault: Boolean) {
/**
* This feature represents whether to deserialize `null` values for collection properties as empty collections.
*/
@@ -30,7 +32,6 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
* Deserializing a singleton overwrites the value of the single instance.
*
* See [jackson-module-kotlin#225]: keep Kotlin singletons as singletons.
- * @see com.fasterxml.jackson.module.kotlin.SingletonSupport
*/
SingletonSupport(enabledByDefault = false),
@@ -41,6 +42,11 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
* may contain null values after deserialization.
* Enabling it protects against this but has significant performance impact.
*/
+ @Deprecated(
+ level = DeprecationLevel.ERROR,
+ message = "This option will be migrated to the new backend in 2.21.",
+ replaceWith = ReplaceWith("NewStrictNullChecks")
+ )
StrictNullChecks(enabledByDefault = false),
/**
@@ -67,13 +73,29 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
* See [jackson-module-kotlin#651] for details.
*/
- UseJavaDurationConversion(enabledByDefault = false);
+ UseJavaDurationConversion(enabledByDefault = false),
+
+ /**
+ * New [StrictNullChecks] feature with improved throughput.
+ * Internally, it will be the same as if [JsonSetter] (contentNulls = FAIL) had been granted.
+ * Benchmarks show that it can check for illegal nulls with throughput nearly identical to the default (see [jackson-module-kotlin#719]).
+ *
+ * Note that in the new backend, the exception thrown has changed from [MissingKotlinParameterException] to [InvalidNullException].
+ * The message will be changed accordingly.
+ * Since 2.19, the base class of [MissingKotlinParameterException] has also been changed to [InvalidNullException],
+ * so be careful when catching it.
+ *
+ * This is a temporary option for a phased backend migration,
+ * which will eventually be merged into [StrictNullChecks].
+ * Also, specifying both this and [StrictNullChecks] is not permitted.
+ */
+ NewStrictNullChecks(enabledByDefault = false);
internal val bitSet: BitSet = (1 shl ordinal).toBitSet()
companion object {
internal val defaults
- get() = values().fold(BitSet(Int.SIZE_BITS)) { acc, cur ->
+ get() = entries.fold(BitSet(Int.SIZE_BITS)) { acc, cur ->
acc.apply { if (cur.enabledByDefault) this.or(cur.bitSet) }
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt
index 70e2e35a6..0879f4232 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeyDeserializers.kt
@@ -5,6 +5,14 @@ import com.fasterxml.jackson.core.exc.InputCoercionException
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializer
import com.fasterxml.jackson.databind.deser.std.StdKeyDeserializers
+import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.reflect.Method
+import java.util.UUID
+import kotlin.reflect.KClass
+import kotlin.reflect.full.primaryConstructor
+import kotlin.reflect.jvm.javaMethod
// The reason why key is treated as nullable is to match the tentative behavior of StdKeyDeserializer.
// If StdKeyDeserializer is modified, need to modify this too.
@@ -65,18 +73,140 @@ internal object ULongKeyDeserializer : StdKeyDeserializer(TYPE_LONG, ULong::clas
}
}
-internal object KotlinKeyDeserializers : StdKeyDeserializers() {
- private fun readResolve(): Any = KotlinKeyDeserializers
+// The implementation is designed to be compatible with various creators, just in case.
+internal sealed class ValueClassKeyDeserializer(
+ converter: ValueClassBoxConverter,
+ creatorHandle: MethodHandle,
+) : KeyDeserializer() {
+ private val boxedClass: Class = converter.boxedClass
+ protected abstract val unboxedClass: Class<*>
+ protected val handle: MethodHandle = MethodHandles.filterReturnValue(creatorHandle, converter.boxHandle)
+
+ // Based on databind error
+ // https://github.com/FasterXML/jackson-databind/blob/341f8d360a5f10b5e609d6ee0ea023bf597ce98a/src/main/java/com/fasterxml/jackson/databind/deser/DeserializerCache.java#L624
+ private fun errorMessage(boxedType: JavaType): String = "Could not find (Map) Key deserializer for types " +
+ "wrapped in $boxedType"
+
+ // Since the input to handle must be strict, invoke should be implemented in each class
+ protected abstract fun invokeExact(value: S): D
+
+ final override fun deserializeKey(key: String?, ctxt: DeserializationContext): Any {
+ val unboxedJavaType = ctxt.constructType(unboxedClass)
+
+ return try {
+ // findKeyDeserializer does not return null, and an exception will be thrown if not found.
+ val value = ctxt.findKeyDeserializer(unboxedJavaType, null).deserializeKey(key, ctxt)
+ @Suppress("UNCHECKED_CAST")
+ invokeExact(value as S)
+ } catch (e: InvalidDefinitionException) {
+ throw JsonMappingException.from(ctxt, errorMessage(ctxt.constructType(boxedClass)), e)
+ }
+ }
+
+ internal sealed class WrapsSpecified(
+ converter: ValueClassBoxConverter,
+ creator: Method,
+ ) : ValueClassKeyDeserializer(
+ converter,
+ // Currently, only the primary constructor can be the creator of a key, so for specified types,
+ // the return type of the primary constructor and the input type of the box function are exactly the same.
+ // Therefore, performance is improved by omitting the asType call.
+ unreflect(creator),
+ )
+
+ internal class WrapsInt(
+ converter: IntValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = Int::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Int): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsLong(
+ converter: LongValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = Long::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: Long): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsString(
+ converter: StringValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = String::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: String?): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsJavaUuid(
+ converter: JavaUuidValueClassBoxConverter,
+ creator: Method,
+ ) : WrapsSpecified(converter, creator) {
+ override val unboxedClass: Class<*> get() = UUID::class.java
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: UUID?): D = handle.invokeExact(value) as D
+ }
+
+ internal class WrapsAny(
+ converter: GenericValueClassBoxConverter,
+ creator: Method,
+ ) : ValueClassKeyDeserializer(
+ converter,
+ unreflectAsType(creator, ANY_TO_ANY_METHOD_TYPE),
+ ) {
+ override val unboxedClass: Class<*> = creator.returnType
+
+ @Suppress("UNCHECKED_CAST")
+ override fun invokeExact(value: S): D = handle.invokeExact(value) as D
+ }
+
+ companion object {
+ fun createOrNull(
+ boxedClass: KClass<*>,
+ cache: ReflectionCache
+ ): ValueClassKeyDeserializer<*, *>? {
+ // primaryConstructor.javaMethod for the value class returns constructor-impl
+ // Only primary constructor is allowed as creator, regardless of visibility.
+ // This is because it is based on the WrapsNullableValueClassBoxDeserializer.
+ // Also, as far as I could research, there was no such functionality as JsonKeyCreator,
+ // so it was not taken into account.
+ val creator = boxedClass.primaryConstructor?.javaMethod ?: return null
+ val converter = cache.getValueClassBoxConverter(creator.returnType, boxedClass)
+
+ return when (converter) {
+ is IntValueClassBoxConverter -> WrapsInt(converter, creator)
+ is LongValueClassBoxConverter -> WrapsLong(converter, creator)
+ is StringValueClassBoxConverter -> WrapsString(converter, creator)
+ is JavaUuidValueClassBoxConverter -> WrapsJavaUuid(converter, creator)
+ is GenericValueClassBoxConverter -> WrapsAny(converter, creator)
+ }
+ }
+ }
+}
+
+internal class KotlinKeyDeserializers(private val cache: ReflectionCache) : StdKeyDeserializers() {
override fun findKeyDeserializer(
type: JavaType,
config: DeserializationConfig?,
beanDesc: BeanDescription?
- ): KeyDeserializer? = when (type.rawClass) {
- UByte::class.java -> UByteKeyDeserializer
- UShort::class.java -> UShortKeyDeserializer
- UInt::class.java -> UIntKeyDeserializer
- ULong::class.java -> ULongKeyDeserializer
- else -> null
+ ): KeyDeserializer? {
+ val rawClass = type.rawClass
+
+ return when {
+ rawClass == UByte::class.java -> UByteKeyDeserializer
+ rawClass == UShort::class.java -> UShortKeyDeserializer
+ rawClass == UInt::class.java -> UIntKeyDeserializer
+ rawClass == ULong::class.java -> ULongKeyDeserializer
+ rawClass.isUnboxableValueClass() -> ValueClassKeyDeserializer.createOrNull(rawClass.kotlin, cache)
+ else -> null
+ }
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt
index c6a5d7747..d6e0b10e6 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinKeySerializers.kt
@@ -9,18 +9,20 @@ import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
import java.lang.reflect.Method
import java.lang.reflect.Modifier
-internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.java) {
- private fun readResolve(): Any = ValueClassUnboxKeySerializer
-
- override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
- val method = value::class.java.getMethod("unbox-impl")
- val unboxed = method.invoke(value)
+internal class ValueClassUnboxKeySerializer(
+ private val converter: ValueClassUnboxConverter,
+) : StdSerializer(converter.valueClass) {
+ override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
+ val unboxed = converter.convert(value)
if (unboxed == null) {
- val javaType = provider.typeFactory.constructType(method.genericReturnType)
+ val javaType = converter.getOutputType(provider.typeFactory)
provider.findNullKeySerializer(javaType, null).serialize(null, gen, provider)
return
}
@@ -29,21 +31,18 @@ internal object ValueClassUnboxKeySerializer : StdSerializer(Any::class.jav
}
}
-// Class must be UnboxableValueClass.
-private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method ->
- Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value }
-}
-
-internal class ValueClassStaticJsonKeySerializer(
- t: Class,
- private val staticJsonKeyGetter: Method
-) : StdSerializer(t) {
- private val keyType: Class<*> = staticJsonKeyGetter.returnType
- private val unboxMethod: Method = t.getMethod("unbox-impl")
+internal sealed class ValueClassStaticJsonKeySerializer(
+ converter: ValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ methodType: MethodType,
+) : StdSerializer(converter.valueClass) {
+ private val keyType: Class<*> = staticJsonValueGetter.returnType
+ private val handle: MethodHandle = unreflectAsType(staticJsonValueGetter, methodType).let {
+ MethodHandles.filterReturnValue(converter.unboxHandle, it)
+ }
- override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
- val unboxed = unboxMethod.invoke(value)
- val jsonKey: Any? = staticJsonKeyGetter.invoke(null, unboxed)
+ final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
+ val jsonKey: Any? = handle.invokeExact(value)
val serializer = jsonKey
?.let { provider.findKeySerializer(keyType, null) }
@@ -52,20 +51,94 @@ internal class ValueClassStaticJsonKeySerializer(
serializer.serialize(jsonKey, gen, provider)
}
+ internal class WrapsInt(
+ converter: IntValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ INT_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsLong(
+ converter: LongValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ LONG_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsString(
+ converter: StringValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ STRING_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsJavaUuid(
+ converter: JavaUuidValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ JAVA_UUID_TO_ANY_METHOD_TYPE,
+ )
+
+ internal class WrapsAny(
+ converter: GenericValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonKeySerializer(
+ converter,
+ staticJsonValueGetter,
+ ANY_TO_ANY_METHOD_TYPE,
+
+ )
+
companion object {
- fun createOrNull(t: Class<*>): StdSerializer<*>? =
- t.getStaticJsonKeyGetter()?.let { ValueClassStaticJsonKeySerializer(t, it) }
+ // Class must be UnboxableValueClass.
+ private fun Class<*>.getStaticJsonKeyGetter(): Method? = this.declaredMethods.find { method ->
+ Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonKey && it.value }
+ }
+
+ // `t` must be UnboxableValueClass.
+ // If create a function with a JsonValue in the value class,
+ // it will be compiled as a static method (= cannot be processed properly by Jackson),
+ // so use a ValueClassSerializer.StaticJsonValue to handle this.
+ fun createOrNull(
+ converter: ValueClassUnboxConverter,
+ ): ValueClassStaticJsonKeySerializer? = converter
+ .valueClass
+ .getStaticJsonKeyGetter()
+ ?.let {
+ when (converter) {
+ is IntValueClassUnboxConverter -> WrapsInt(converter, it)
+ is LongValueClassUnboxConverter -> WrapsLong(converter, it)
+ is StringValueClassUnboxConverter -> WrapsString(converter, it)
+ is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it)
+ is GenericValueClassUnboxConverter -> WrapsAny(converter, it)
+ }
+ }
}
}
-internal class KotlinKeySerializers : Serializers.Base() {
+internal class KotlinKeySerializers(private val cache: ReflectionCache) : Serializers.Base() {
override fun findSerializer(
config: SerializationConfig,
type: JavaType,
- beanDesc: BeanDescription
- ): JsonSerializer<*>? = when {
- type.rawClass.isUnboxableValueClass() -> ValueClassStaticJsonKeySerializer.createOrNull(type.rawClass)
- ?: ValueClassUnboxKeySerializer
- else -> null
+ beanDesc: BeanDescription,
+ ): JsonSerializer<*>? {
+ val rawClass = type.rawClass
+
+ return when {
+ rawClass.isUnboxableValueClass() -> {
+ val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
+ ValueClassStaticJsonKeySerializer.createOrNull(unboxConverter)
+ ?: ValueClassUnboxKeySerializer(unboxConverter)
+ }
+ else -> null
+ }
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt
index b6d4143bd..d23afd4ef 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt
@@ -1,125 +1,109 @@
package com.fasterxml.jackson.module.kotlin
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.module.kotlin.KotlinFeature.KotlinPropertyNameAsImplicitName
+import com.fasterxml.jackson.module.kotlin.KotlinFeature.NewStrictNullChecks
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection
import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap
+import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport
import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks
-import com.fasterxml.jackson.module.kotlin.KotlinFeature.KotlinPropertyNameAsImplicitName
import com.fasterxml.jackson.module.kotlin.KotlinFeature.UseJavaDurationConversion
-import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE
-import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED
import java.util.*
-import kotlin.reflect.KClass
-
-private const val metadataFqName = "kotlin.Metadata"
-fun Class<*>.isKotlinClass(): Boolean {
- return declaredAnnotations.any { it.annotationClass.java.name == metadataFqName }
-}
+fun Class<*>.isKotlinClass(): Boolean = this.isAnnotationPresent(Metadata::class.java)
/**
- * @param reflectionCacheSize Default: 512. Size, in items, of the caches used for mapping objects.
- * @param nullToEmptyCollection Default: false. Whether to deserialize null values for collection properties as
- * empty collections.
- * @param nullToEmptyMap Default: false. Whether to deserialize null values for a map property to an empty
- * map object.
- * @param nullIsSameAsDefault Default false. Whether to treat null values as absent when deserializing, thereby
- * using the default value provided in Kotlin.
- * @param singletonSupport Default: DISABLED. Mode for singleton handling.
- * See {@link com.fasterxml.jackson.module.kotlin.SingletonSupport label}
- * @param strictNullChecks Default: false. Whether to check deserialized collections. With this disabled,
- * the default, collections which are typed to disallow null members
- * (e.g. List) may contain null values after deserialization. Enabling it
- * protects against this but has significant performance impact.
- * @param useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
- * This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
+ * @constructor To avoid binary compatibility issues, the primary constructor is not published.
+ * Please use KotlinModule.Builder or extensions that use it.
+ * @property reflectionCacheSize Default: 512. Size, in items, of the caches used for mapping objects.
+ * @property nullToEmptyCollection Default: false. Whether to deserialize null values for collection properties as
+ * empty collections.
+ * @property nullToEmptyMap Default: false. Whether to deserialize null values for a map property to an empty
+ * map object.
+ * @property nullIsSameAsDefault Default false. Whether to treat null values as absent when deserializing, thereby
+ * using the default value provided in Kotlin.
+ * @property singletonSupport Default: false. Mode for singleton handling.
+ * See [KotlinFeature.SingletonSupport]
+ * @property enabledSingletonSupport Default: false. A temporary property that is maintained until the return value of `singletonSupport` is changed.
+ * It will be removed in 2.21.
+ * @property strictNullChecks Default: false. Whether to check deserialized collections. With this disabled,
+ * the default, collections which are typed to disallow null members
+ * (e.g. List) may contain null values after deserialization. Enabling it
+ * protects against this but has significant performance impact.
+ * @property kotlinPropertyNameAsImplicitName Default: false. Whether to use the Kotlin property name as the implicit name.
+ * See [KotlinFeature.KotlinPropertyNameAsImplicitName] for details.
+ * @property useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
+ * This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
*/
-class KotlinModule @Deprecated(
- level = DeprecationLevel.WARNING,
- message = "Use KotlinModule.Builder instead of named constructor parameters.",
- replaceWith = ReplaceWith(
- """KotlinModule.Builder()
- .withReflectionCacheSize(reflectionCacheSize)
- .configure(KotlinFeature.NullToEmptyCollection, nullToEmptyCollection)
- .configure(KotlinFeature.NullToEmptyMap, nullToEmptyMap)
- .configure(KotlinFeature.NullIsSameAsDefault, nullIsSameAsDefault)
- .configure(KotlinFeature.SingletonSupport, singletonSupport)
- .configure(KotlinFeature.StrictNullChecks, strictNullChecks)
- .build()""",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
-) constructor(
- val reflectionCacheSize: Int = 512,
- val nullToEmptyCollection: Boolean = false,
- val nullToEmptyMap: Boolean = false,
- val nullIsSameAsDefault: Boolean = false,
- val singletonSupport: SingletonSupport = DISABLED,
- val strictNullChecks: Boolean = false,
- val useKotlinPropertyNameForGetter: Boolean = false,
- val useJavaDurationConversion: Boolean = false,
+class KotlinModule private constructor(
+ val reflectionCacheSize: Int = Builder.DEFAULT_CACHE_SIZE,
+ val nullToEmptyCollection: Boolean = NullToEmptyCollection.enabledByDefault,
+ val nullToEmptyMap: Boolean = NullToEmptyMap.enabledByDefault,
+ val nullIsSameAsDefault: Boolean = NullIsSameAsDefault.enabledByDefault,
+ val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
+ @Suppress("DEPRECATION_ERROR")
+ strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
+ val kotlinPropertyNameAsImplicitName: Boolean = KotlinPropertyNameAsImplicitName.enabledByDefault,
+ val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault,
+ private val newStrictNullChecks: Boolean = NewStrictNullChecks.enabledByDefault,
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
- companion object {
- // Increment when option is added
- const val serialVersionUID = 2L
- }
+ /*
+ * Prior to 2.18, an older Enum called SingletonSupport was used to manage feature.
+ * To deprecate it and replace it with singletonSupport: Boolean, the following steps are in progress.
+ *
+ * 1. add enabledSingletonSupport: Boolean property
+ * 2. delete SingletonSupport class and change the property to singletonSupport: Boolean
+ * 3. remove the enabledSingletonSupport property
+ *
+ * Now that 2 is complete, deprecation is in progress for 3.
+ */
+ @Deprecated(
+ level = DeprecationLevel.ERROR,
+ message = "This property is scheduled to be removed in 2.21 or later" +
+ " in order to unify the use of KotlinFeature.",
+ replaceWith = ReplaceWith("singletonSupport")
+ )
+ val enabledSingletonSupport: Boolean get() = singletonSupport
- init {
- if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) {
- // Kotlin 1.4 was deprecated when this process was introduced(jackson-module-kotlin 2.15).
- throw JsonMappingException(
- null,
- "KotlinModule requires Kotlin version >= 1.5 - Found ${KotlinVersion.CURRENT}"
- )
+ private val oldStrictNullChecks: Boolean = strictNullChecks
+
+ // To reduce the amount of destructive changes, no properties will be added to the public.
+ val strictNullChecks: Boolean = if (strictNullChecks) {
+ if (newStrictNullChecks) {
+ throw IllegalArgumentException("Enabling both StrictNullChecks and NewStrictNullChecks is not permitted.")
}
+
+ true
+ } else {
+ newStrictNullChecks
}
- @Deprecated(level = DeprecationLevel.HIDDEN, message = "For ABI compatibility")
- constructor(
- reflectionCacheSize: Int,
- nullToEmptyCollection: Boolean,
- nullToEmptyMap: Boolean
- ) : this(
- Builder()
- .withReflectionCacheSize(reflectionCacheSize)
- .configure(NullToEmptyCollection, nullToEmptyCollection)
- .configure(NullToEmptyMap, nullToEmptyMap)
- .disable(NullIsSameAsDefault)
- )
+ companion object {
+ // Increment when option is added
+ private const val serialVersionUID = 3L
+ }
- @Deprecated(level = DeprecationLevel.HIDDEN, message = "For ABI compatibility")
- constructor(
- reflectionCacheSize: Int,
- nullToEmptyCollection: Boolean,
- nullToEmptyMap: Boolean,
- nullIsSameAsDefault: Boolean
- ) : this(
- Builder()
- .withReflectionCacheSize(reflectionCacheSize)
- .configure(NullToEmptyCollection, nullToEmptyCollection)
- .configure(NullToEmptyMap, nullToEmptyMap)
- .configure(NullIsSameAsDefault, nullIsSameAsDefault)
+ @Deprecated(
+ level = DeprecationLevel.HIDDEN,
+ message = "If you have no choice but to initialize KotlinModule from reflection, use this constructor."
)
+ constructor() : this()
- @Suppress("DEPRECATION")
private constructor(builder: Builder) : this(
builder.reflectionCacheSize,
builder.isEnabled(NullToEmptyCollection),
builder.isEnabled(NullToEmptyMap),
builder.isEnabled(NullIsSameAsDefault),
- when {
- builder.isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE
- else -> DISABLED
- },
+ builder.isEnabled(SingletonSupport),
+ @Suppress("DEPRECATION_ERROR")
builder.isEnabled(StrictNullChecks),
builder.isEnabled(KotlinPropertyNameAsImplicitName),
builder.isEnabled(UseJavaDurationConversion),
+ builder.isEnabled(NewStrictNullChecks),
)
- private val ignoredClassesForImplyingJsonCreator = emptySet>()
-
override fun setupModule(context: SetupContext) {
super.setupModule(context)
@@ -129,13 +113,10 @@ class KotlinModule @Deprecated(
val cache = ReflectionCache(reflectionCacheSize)
- context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, strictNullChecks))
+ context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, oldStrictNullChecks))
- when (singletonSupport) {
- DISABLED -> Unit
- CANONICALIZE -> {
- context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier)
- }
+ if (singletonSupport) {
+ context.addBeanDeserializerModifier(KotlinBeanDeserializerModifier)
}
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
@@ -147,31 +128,24 @@ class KotlinModule @Deprecated(
useJavaDurationConversion
))
context.appendAnnotationIntrospector(
- KotlinNamesAnnotationIntrospector(
- this,
- cache,
- ignoredClassesForImplyingJsonCreator,
- useKotlinPropertyNameForGetter)
+ KotlinNamesAnnotationIntrospector(cache, newStrictNullChecks, kotlinPropertyNameAsImplicitName)
)
- context.addDeserializers(KotlinDeserializers(useJavaDurationConversion))
- context.addKeyDeserializers(KotlinKeyDeserializers)
- context.addSerializers(KotlinSerializers())
- context.addKeySerializers(KotlinKeySerializers())
-
- fun addMixIn(clazz: Class<*>, mixin: Class<*>) {
- context.setMixInAnnotations(clazz, mixin)
- }
+ context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
+ context.addKeyDeserializers(KotlinKeyDeserializers(cache))
+ context.addSerializers(KotlinSerializers(cache))
+ context.addKeySerializers(KotlinKeySerializers(cache))
// ranges
- addMixIn(IntRange::class.java, ClosedRangeMixin::class.java)
- addMixIn(CharRange::class.java, ClosedRangeMixin::class.java)
- addMixIn(LongRange::class.java, ClosedRangeMixin::class.java)
- addMixIn(ClosedRange::class.java, ClosedRangeMixin::class.java)
+ context.setMixInAnnotations(ClosedRange::class.java, ClosedRangeMixin::class.java)
}
class Builder {
- var reflectionCacheSize: Int = 512
+ companion object {
+ internal const val DEFAULT_CACHE_SIZE = 512
+ }
+
+ var reflectionCacheSize: Int = DEFAULT_CACHE_SIZE
private set
private val bitSet: BitSet = KotlinFeature.defaults
@@ -197,119 +171,6 @@ class KotlinModule @Deprecated(
fun isEnabled(feature: KotlinFeature): Boolean =
bitSet.intersects(feature.bitSet)
- @Deprecated(
- message = "Deprecated, use withReflectionCacheSize(reflectionCacheSize) instead.",
- replaceWith = ReplaceWith("withReflectionCacheSize(reflectionCacheSize)")
- )
- fun reflectionCacheSize(reflectionCacheSize: Int): Builder =
- withReflectionCacheSize(reflectionCacheSize)
-
- @Deprecated(
- message = "Deprecated, use isEnabled(NullToEmptyCollection) instead.",
- replaceWith = ReplaceWith(
- "isEnabled(KotlinFeature.NullToEmptyCollection)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun getNullToEmptyCollection(): Boolean =
- isEnabled(NullToEmptyCollection)
-
- @Deprecated(
- message = "Deprecated, use configure(NullToEmptyCollection, enabled) instead.",
- replaceWith = ReplaceWith(
- "configure(KotlinFeature.NullToEmptyCollection, nullToEmptyCollection)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun nullToEmptyCollection(nullToEmptyCollection: Boolean): Builder =
- configure(NullToEmptyCollection, nullToEmptyCollection)
-
- @Deprecated(
- message = "Deprecated, use isEnabled(NullToEmptyMap) instead.",
- replaceWith = ReplaceWith(
- "isEnabled(KotlinFeature.NullToEmptyMap)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun getNullToEmptyMap(): Boolean =
- isEnabled(NullToEmptyMap)
-
- @Deprecated(
- message = "Deprecated, use configure(NullToEmptyMap, enabled) instead.",
- replaceWith = ReplaceWith(
- "configure(KotlinFeature.NullToEmptyMap, nullToEmptyMap)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun nullToEmptyMap(nullToEmptyMap: Boolean): Builder =
- configure(NullToEmptyMap, nullToEmptyMap)
-
- @Deprecated(
- message = "Deprecated, use isEnabled(NullIsSameAsDefault) instead.",
- replaceWith = ReplaceWith(
- "isEnabled(KotlinFeature.NullIsSameAsDefault)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun getNullIsSameAsDefault(): Boolean =
- isEnabled(NullIsSameAsDefault)
-
- @Deprecated(
- message = "Deprecated, use configure(NullIsSameAsDefault, enabled) instead.",
- replaceWith = ReplaceWith(
- "configure(KotlinFeature.NullIsSameAsDefault, nullIsSameAsDefault)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun nullIsSameAsDefault(nullIsSameAsDefault: Boolean): Builder =
- configure(NullIsSameAsDefault, nullIsSameAsDefault)
-
- @Deprecated(
- message = "Deprecated, use isEnabled(SingletonSupport) instead.",
- replaceWith = ReplaceWith(
- "isEnabled(KotlinFeature.SingletonSupport)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun getSingletonSupport(): SingletonSupport =
- when {
- isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE
- else -> DISABLED
- }
-
- @Deprecated(
- message = "Deprecated, use configure(SingletonSupport, enabled) instead.",
- replaceWith = ReplaceWith(
- "configure(KotlinFeature.SingletonSupport, singletonSupport)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun singletonSupport(singletonSupport: SingletonSupport): Builder =
- when (singletonSupport) {
- CANONICALIZE -> enable(KotlinFeature.SingletonSupport)
- else -> disable(KotlinFeature.SingletonSupport)
- }
-
- @Deprecated(
- message = "Deprecated, use isEnabled(StrictNullChecks) instead.",
- replaceWith = ReplaceWith(
- "isEnabled(KotlinFeature.StrictNullChecks)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun getStrictNullChecks(): Boolean =
- isEnabled(StrictNullChecks)
-
- @Deprecated(
- message = "Deprecated, use configure(StrictNullChecks, enabled) instead.",
- replaceWith = ReplaceWith(
- "configure(KotlinFeature.StrictNullChecks, strictNullChecks)",
- "com.fasterxml.jackson.module.kotlin.KotlinFeature"
- )
- )
- fun strictNullChecks(strictNullChecks: Boolean): Builder =
- configure(StrictNullChecks, strictNullChecks)
-
fun build(): KotlinModule =
KotlinModule(this)
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
index aaca6ccb5..c6f3403b9 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
@@ -1,34 +1,32 @@
package com.fasterxml.jackson.module.kotlin
-import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSetter
+import com.fasterxml.jackson.annotation.Nulls
+import com.fasterxml.jackson.databind.JavaType
+import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.Annotated
-import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor
+import com.fasterxml.jackson.databind.introspect.AnnotatedClass
import com.fasterxml.jackson.databind.introspect.AnnotatedMember
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
+import com.fasterxml.jackson.databind.introspect.PotentialCreator
import java.lang.reflect.Constructor
-import java.lang.reflect.Method
import java.util.Locale
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
-import kotlin.reflect.full.companionObject
-import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
-import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.javaType
-import kotlin.reflect.jvm.kotlinFunction
internal class KotlinNamesAnnotationIntrospector(
- val module: KotlinModule,
- val cache: ReflectionCache,
- val ignoredClassesForImplyingJsonCreator: Set>,
- val useKotlinPropertyNameForGetter: Boolean
+ private val cache: ReflectionCache,
+ private val strictNullChecks: Boolean,
+ private val kotlinPropertyNameAsImplicitName: Boolean
) : NopAnnotationIntrospector() {
private fun getterNameFromJava(member: AnnotatedMethod): String? {
val name = member.name
@@ -57,8 +55,7 @@ internal class KotlinNamesAnnotationIntrospector(
return member.member.declaringClass.takeIf { it.isKotlinClass() }?.let { clazz ->
// For edge case, methods must be compared by name, not directly.
- clazz.kotlin.memberProperties.find { it.javaGetter?.name == getterName }
- ?.let { it.name }
+ clazz.kotlin.memberProperties.find { it.javaGetter?.name == getterName }?.name
}
}
@@ -68,7 +65,7 @@ internal class KotlinNamesAnnotationIntrospector(
return when (member) {
is AnnotatedMethod -> if (member.parameterCount == 0) {
- if (useKotlinPropertyNameForGetter) {
+ if (kotlinPropertyNameAsImplicitName) {
// Fall back to default if it is a getter-like function
getterNameFromKotlin(member) ?: getterNameFromJava(member)
} else getterNameFromJava(member)
@@ -78,96 +75,76 @@ internal class KotlinNamesAnnotationIntrospector(
}
}
- @Suppress("UNCHECKED_CAST")
- private fun hasCreatorAnnotation(member: AnnotatedConstructor): Boolean {
- // don't add a JsonCreator to any constructor if one is declared already
-
- val kClass = member.declaringClass.kotlin
- .apply { if (this in ignoredClassesForImplyingJsonCreator) return false }
- val kConstructor = cache.kotlinFromJava(member.annotated as Constructor) ?: return false
-
- // TODO: should we do this check or not? It could cause failures if we miss another way a property could be set
- // val requiredProperties = kClass.declaredMemberProperties.filter {!it.returnType.isMarkedNullable }.map { it.name }.toSet()
- // val areAllRequiredParametersInConstructor = kConstructor.parameters.all { requiredProperties.contains(it.name) }
-
- val propertyNames = kClass.memberProperties.map { it.name }.toSet()
-
- return when {
- kConstructor.isPossibleSingleString(propertyNames) -> false
- kConstructor.parameters.any { it.name == null } -> false
- !kClass.isPrimaryConstructor(kConstructor) -> false
- else -> {
- val anyConstructorHasJsonCreator = kClass.constructors
- .filterOutSingleStringCallables(propertyNames)
- .any { it.hasAnnotation() }
-
- val anyCompanionMethodIsJsonCreator = member.type.rawClass.kotlin.companionObject?.declaredFunctions
- ?.filterOutSingleStringCallables(propertyNames)
- ?.any { it.hasAnnotation() && it.hasAnnotation() }
- ?: false
-
- !(anyConstructorHasJsonCreator || anyCompanionMethodIsJsonCreator)
- }
- }
- }
-
- override fun hasCreatorAnnotation(member: Annotated): Boolean =
- if (member is AnnotatedConstructor && member.isKotlinConstructorWithParameters())
- cache.checkConstructorIsCreatorAnnotated(member) { hasCreatorAnnotation(it) }
- else
- false
-
- @Suppress("UNCHECKED_CAST")
- private fun findKotlinParameterName(param: AnnotatedParameter): String? {
- return if (param.declaringClass.isKotlinClass()) {
- val member = param.owner.member
- if (member is Constructor<*>) {
- val ctor = (member as Constructor)
- val ctorParmCount = ctor.parameterTypes.size
- val ktorParmCount = try { ctor.kotlinFunction?.parameters?.size ?: 0 }
- catch (ex: KotlinReflectionInternalError) { 0 }
- catch (ex: UnsupportedOperationException) { 0 }
- if (ktorParmCount > 0 && ktorParmCount == ctorParmCount) {
- ctor.kotlinFunction?.parameters?.get(param.index)?.name
+ override fun refineDeserializationType(config: MapperConfig<*>, a: Annotated, baseType: JavaType): JavaType =
+ findKotlinParameter(a)?.let { param ->
+ val rawType = a.rawType
+ (param.type.classifier as? KClass<*>)
+ ?.java
+ ?.takeIf { it.isUnboxableValueClass() && it != rawType }
+ ?.let { config.constructType(it) }
+ } ?: baseType
+
+ override fun findSetterInfo(ann: Annotated): JsonSetter.Value = ann.takeIf { strictNullChecks }
+ ?.let { _ ->
+ findKotlinParameter(ann)?.let { param ->
+ if (param.requireStrictNullCheck(ann.type)) {
+ JsonSetter.Value.forContentNulls(Nulls.FAIL)
} else {
null
}
- } else if (member is Method) {
- try {
- val temp = member.kotlinFunction
-
- val firstParamKind = temp?.parameters?.firstOrNull()?.kind
- val idx = if (firstParamKind != KParameter.Kind.VALUE) param.index + 1 else param.index
- val parmCount = temp?.parameters?.size ?: 0
- if (parmCount > idx) {
- temp?.parameters?.get(idx)?.name
- } else {
- null
- }
- } catch (ex: KotlinReflectionInternalError) {
- null
- }
- } else {
- null
}
- } else {
- null
+ }
+ ?: super.findSetterInfo(ann)
+
+ override fun findDefaultCreator(
+ config: MapperConfig<*>,
+ valueClass: AnnotatedClass,
+ declaredConstructors: List,
+ declaredFactories: List
+ ): PotentialCreator? {
+ val kClass = valueClass.creatableKotlinClass() ?: return null
+
+ val defaultCreator = kClass.primarilyConstructor()
+ ?.takeIf { ctor ->
+ val propertyNames = kClass.memberProperties.map { it.name }.toSet()
+ ctor.isPossibleCreator(propertyNames)
+ }
+ ?: return null
+
+ return declaredConstructors.find {
+ // To avoid problems with constructors that include `value class` as an argument,
+ // convert to `KFunction` and compare
+ cache.kotlinFromJava(it.creator().annotated as Constructor<*>) == defaultCreator
}
}
+
+ private fun findKotlinParameterName(param: AnnotatedParameter): String? = cache.findKotlinParameter(param)?.name
+
+ private fun findKotlinParameter(param: Annotated) = (param as? AnnotatedParameter)
+ ?.let { cache.findKotlinParameter(it) }
}
-// if has parameters, is a Kotlin class, and the parameters all have parameter annotations, then pretend we have a JsonCreator
-private fun AnnotatedConstructor.isKotlinConstructorWithParameters(): Boolean =
- parameterCount > 0 && declaringClass.isKotlinClass() && !declaringClass.isEnum
+private fun KParameter.markedNonNullAt(index: Int) = type.arguments.getOrNull(index)?.type?.isMarkedNullable == false
-private fun KFunction<*>.isPossibleSingleString(propertyNames: Set): Boolean = parameters.size == 1 &&
- parameters[0].name !in propertyNames &&
- parameters[0].type.javaType == String::class.java &&
- !parameters[0].hasAnnotation()
+private fun KParameter.requireStrictNullCheck(type: JavaType): Boolean =
+ ((type.isArrayType || type.isCollectionLikeType) && this.markedNonNullAt(0)) ||
+ (type.isMapLikeType && this.markedNonNullAt(1))
-private fun Collection>.filterOutSingleStringCallables(propertyNames: Set): Collection> =
- this.filter { !it.isPossibleSingleString(propertyNames) }
-private fun KClass<*>.isPrimaryConstructor(kConstructor: KFunction<*>) = this.primaryConstructor.let {
- it == kConstructor || (it == null && this.constructors.size == 1)
-}
+// If it is not a Kotlin class or an Enum, Creator is not used
+private fun AnnotatedClass.creatableKotlinClass(): KClass<*>? = annotated
+ .takeIf { it.isKotlinClass() && !it.isEnum }
+ ?.kotlin
+
+// By default, the primary constructor or the only publicly available constructor may be used
+private fun KClass<*>.primarilyConstructor() = primaryConstructor ?: constructors.singleOrNull()
+
+private fun KFunction<*>.isPossibleCreator(propertyNames: Set): Boolean = 0 < parameters.size
+ && !isPossibleSingleString(propertyNames)
+ && parameters.none { it.name == null }
+
+private fun KFunction<*>.isPossibleSingleString(propertyNames: Set): Boolean = parameters.singleOrNull()?.let {
+ it.name !in propertyNames
+ && it.type.javaType == String::class.java
+ && !it.hasAnnotation()
+} == true
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt
index edfe23eaa..42aabc2fa 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinSerializers.kt
@@ -9,20 +9,12 @@ import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import java.lang.invoke.MethodHandle
+import java.lang.invoke.MethodHandles
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.math.BigInteger
-@Deprecated(
- message = "This class will be removed in 2.16 or later as it has been replaced by SequenceToIteratorConverter.",
- replaceWith = ReplaceWith("com.fasterxml.jackson.module.kotlin.SequenceToIteratorConverter")
-)
-object SequenceSerializer : StdSerializer>(Sequence::class.java) {
- override fun serialize(value: Sequence<*>, gen: JsonGenerator, provider: SerializerProvider) {
- provider.defaultSerializeValue(value.iterator(), gen)
- }
-}
-
object UByteSerializer : StdSerializer(UByte::class.java) {
private fun readResolve(): Any = UByteSerializer
@@ -56,54 +48,98 @@ object ULongSerializer : StdSerializer(ULong::class.java) {
}
}
-// Class must be UnboxableValueClass.
-private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.find { method ->
- Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value }
-}
-
+@Deprecated(
+ message = "This class was published by mistake. It will be removed in `2.22.0` as it is no longer used internally.",
+ level = DeprecationLevel.WARNING
+)
object ValueClassUnboxSerializer : StdSerializer(Any::class.java) {
private fun readResolve(): Any = ValueClassUnboxSerializer
override fun serialize(value: Any, gen: JsonGenerator, provider: SerializerProvider) {
val unboxed = value::class.java.getMethod("unbox-impl").invoke(value)
-
- if (unboxed == null) {
- provider.findNullValueSerializer(null).serialize(null, gen, provider)
- return
- }
-
- provider.findValueSerializer(unboxed::class.java).serialize(unboxed, gen, provider)
+ provider.defaultSerializeValue(unboxed, gen)
}
}
-internal sealed class ValueClassSerializer(t: Class) : StdSerializer(t) {
- class StaticJsonValue(
- t: Class, private val staticJsonValueGetter: Method
- ) : ValueClassSerializer(t) {
- private val unboxMethod: Method = t.getMethod("unbox-impl")
-
- override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
- val unboxed = unboxMethod.invoke(value)
- // As shown in the processing of the factory function, jsonValueGetter is always a static method.
- val jsonValue: Any? = staticJsonValueGetter.invoke(null, unboxed)
- jsonValue
- ?.let { provider.findValueSerializer(it::class.java).serialize(it, gen, provider) }
- ?: provider.findNullValueSerializer(null).serialize(null, gen, provider)
- }
+// Class must be UnboxableValueClass.
+private fun Class<*>.getStaticJsonValueGetter(): Method? = this.declaredMethods.find { method ->
+ Modifier.isStatic(method.modifiers) && method.annotations.any { it is JsonValue && it.value }
+}
+
+internal sealed class ValueClassStaticJsonValueSerializer(
+ converter: ValueClassUnboxConverter,
+ staticJsonValueHandle: MethodHandle,
+) : StdSerializer(converter.valueClass) {
+ private val handle: MethodHandle = MethodHandles.filterReturnValue(converter.unboxHandle, staticJsonValueHandle)
+
+ final override fun serialize(value: T, gen: JsonGenerator, provider: SerializerProvider) {
+ val jsonValue: Any? = handle.invokeExact(value)
+ provider.defaultSerializeValue(jsonValue, gen)
}
+ internal class WrapsInt(
+ converter: IntValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, INT_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsLong(
+ converter: LongValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, LONG_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsString(
+ converter: StringValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, STRING_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsJavaUuid(
+ converter: JavaUuidValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, JAVA_UUID_TO_ANY_METHOD_TYPE),
+ )
+
+ internal class WrapsAny(
+ converter: GenericValueClassUnboxConverter,
+ staticJsonValueGetter: Method,
+ ) : ValueClassStaticJsonValueSerializer(
+ converter,
+ unreflectAsType(staticJsonValueGetter, ANY_TO_ANY_METHOD_TYPE),
+ )
+
companion object {
// `t` must be UnboxableValueClass.
// If create a function with a JsonValue in the value class,
// it will be compiled as a static method (= cannot be processed properly by Jackson),
// so use a ValueClassSerializer.StaticJsonValue to handle this.
- fun from(t: Class<*>): StdSerializer<*> = t.getStaticJsonValueGetter()
- ?.let { StaticJsonValue(t, it) }
- ?: ValueClassUnboxSerializer
+ fun createOrNull(
+ converter: ValueClassUnboxConverter,
+ ): ValueClassStaticJsonValueSerializer? = converter
+ .valueClass
+ .getStaticJsonValueGetter()
+ ?.let {
+ when (converter) {
+ is IntValueClassUnboxConverter -> WrapsInt(converter, it)
+ is LongValueClassUnboxConverter -> WrapsLong(converter, it)
+ is StringValueClassUnboxConverter -> WrapsString(converter, it)
+ is JavaUuidValueClassUnboxConverter -> WrapsJavaUuid(converter, it)
+ is GenericValueClassUnboxConverter -> WrapsAny(converter, it)
+ }
+ }
}
}
-internal class KotlinSerializers : Serializers.Base() {
+internal class KotlinSerializers(private val cache: ReflectionCache) : Serializers.Base() {
override fun findSerializer(
config: SerializationConfig?,
type: JavaType,
@@ -117,7 +153,10 @@ internal class KotlinSerializers : Serializers.Base() {
UInt::class.java == rawClass -> UIntSerializer
ULong::class.java == rawClass -> ULongSerializer
// The priority of Unboxing needs to be lowered so as not to break the serialization of Unsigned Integers.
- rawClass.isUnboxableValueClass() -> ValueClassSerializer.from(rawClass)
+ rawClass.isUnboxableValueClass() -> {
+ val unboxConverter = cache.getValueClassUnboxConverter(rawClass)
+ ValueClassStaticJsonValueSerializer.createOrNull(unboxConverter) ?: unboxConverter.delegatingSerializer
+ }
else -> null
}
}
diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt
index 8c473717d..27e6f0dcc 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt
@@ -5,15 +5,14 @@ import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
+import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.SettableBeanProperty
import com.fasterxml.jackson.databind.deser.ValueInstantiator
import com.fasterxml.jackson.databind.deser.ValueInstantiators
-import com.fasterxml.jackson.databind.deser.impl.NullsAsEmptyProvider
import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
-import com.fasterxml.jackson.databind.exc.MismatchedInputException
+import com.fasterxml.jackson.databind.exc.InvalidNullException
import java.lang.reflect.TypeVariable
-import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection
import kotlin.reflect.jvm.javaType
@@ -33,6 +32,15 @@ internal class KotlinValueInstantiator(
private fun List.markedNonNullAt(index: Int) = getOrNull(index)?.type?.isMarkedNullable == false
+ // If the argument is a value class that wraps nullable and non-null,
+ // and the input is explicit null, the value class is instantiated with null as input.
+ private fun requireValueClassSpecialNullValue(
+ isNullableParam: Boolean,
+ valueDeserializer: JsonDeserializer<*>?
+ ): Boolean = !isNullableParam &&
+ valueDeserializer is WrapsNullableValueClassDeserializer<*> &&
+ valueDeserializer.handledType().kotlin.wrapsNullable()
+
private fun SettableBeanProperty.skipNulls(): Boolean =
nullIsSameAsDefault || (metadata.valueNulls == Nulls.SKIP)
@@ -44,47 +52,35 @@ internal class KotlinValueInstantiator(
val valueCreator: ValueCreator<*> = cache.valueCreatorFromJava(_withArgsCreator)
?: return super.createFromObjectWith(ctxt, props, buffer)
- val propCount: Int
- var numCallableParameters: Int
- val callableParameters: Array