From 6da3b4a712cd36b13ec6d8d60335e28e4f036e12 Mon Sep 17 00:00:00 2001 From: Kartikaya Gupta Date: Fri, 31 Jul 2026 21:00:13 -0400 Subject: [PATCH] Migrate to Jackson 3.1.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the Jackson BOM from 2.21.2 to 3.1.5 and moves the five Jackson-using source files onto the `tools.jackson` packages. `jackson-annotations` is deliberately left on `com.fasterxml` — it is not renamed in 3.x and the 3.1.5 BOM pins it to 2.21, which is what lets Jackson 2 and 3 coexist on a classpath. `jackson-datatype-jsr310` is dropped; java.time support is folded into databind in 3.x. Four changes are behavioural rather than mechanical: - Mappers are immutable in 3.x, but `SecretDeserializer` and `ResourceAwareDeserializer` parse nested documents with the very mapper they are registered on. They now take a `() -> ObjectMapper` supplier that resolves once `builder.build()` returns. `SecretJacksonModule` keeps its `ObjectMapper` constructor and exposes `mapper` as a computed property, so its source shape is unchanged. - `FAIL_ON_UNKNOWN_PROPERTIES` defaults to false in 3.x. Left alone, MiskConfig's "'x' not found in Config, did you mean...?" warning would silently never fire again and config typos would be ignored. It is now explicitly enabled for the first parse attempt; the retry path used to relax the mapper in place and instead rebuilds one. - `SORT_PROPERTIES_ALPHABETICALLY` and `EnumFeature.READ/WRITE_ENUMS_USING_TO_STRING` default on in 3.x and are pinned off. Sorting would reshuffle every service's redacted config dashboard, and config enums are matched by name while `toString()` is frequently overridden for display. - `KotlinFeature.StrictNullChecks` defaults on in 3.x and is pinned off. With it on a null element of a collection nested inside a map is rejected even when that element type is declared nullable, so `Map>` fails where a top-level `Set` is accepted. Config that loads today would stop loading. This is a breaking change for consumers. The affected ABI, confirmed by the regenerated api dumps, is limited to: the `MiskConfig.load` overloads taking `JsonNode`/`ValueDeserializerModifier`, the three `SimpleModule` subclasses in misk-config, and `BackwardsCompatibleClientsConfigConverter`. Both misk-config and misk expose Jackson via `api(...)`, so consumers relying on the transitive dependency inherit Jackson 3. --- gradle/libs.versions.toml | 13 +- misk-config/api/misk-config.api | 33 +-- misk-config/build.gradle.kts | 1 - .../src/main/kotlin/misk/config/MiskConfig.kt | 217 +++++++++++------- .../test/kotlin/misk/config/MiskConfigTest.kt | 4 +- .../org/assertj/core/api/AssertExtensions.kt | 8 +- misk/api/misk.api | 6 +- .../kotlin/misk/client/HttpClientsConfig.kt | 2 +- ...HttpClientsConfigBackwardsCompatibility.kt | 6 +- 9 files changed, 167 insertions(+), 123 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09e9529ae5d..58153581814 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ googleAuth = "1.39.1" googleHttp = "2.0.0" guava = "33.5.0-jre" hoplite = "2.7.5" -jackson = "2.21.2" +jackson = "3.1.5" jooq = "3.19.29" junit = "5.14.2" kotest = "6.0.7" @@ -123,12 +123,11 @@ hopliteYaml = { module = "com.sksamuel.hoplite:hoplite-yaml", version.ref = "hop hsqldb = { module = "org.hsqldb:hsqldb", version = "2.7.4" } jCommander = { module = "com.beust:jcommander", version = "1.82" } jacksonAnnotations = { module = "com.fasterxml.jackson.core:jackson-annotations" } -jacksonBom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" } -jacksonCore = { module = "com.fasterxml.jackson.core:jackson-core" } -jacksonDatabind = { module = "com.fasterxml.jackson.core:jackson-databind" } -jacksonDataformatYaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" } -jacksonJsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" } -jacksonKotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin" } +jacksonBom = { module = "tools.jackson:jackson-bom", version.ref = "jackson" } +jacksonCore = { module = "tools.jackson.core:jackson-core" } +jacksonDatabind = { module = "tools.jackson.core:jackson-databind" } +jacksonDataformatYaml = { module = "tools.jackson.dataformat:jackson-dataformat-yaml" } +jacksonKotlin = { module = "tools.jackson.module:jackson-module-kotlin" } jakartaInject = { module = "jakarta.inject:jakarta.inject-api", version = "2.0.1" } javaxAnnotation = { module = "javax.annotation:javax.annotation-api", version = "1.3.2" } javaxInject = { module = "javax.inject:javax.inject", version = "1" } diff --git a/misk-config/api/misk-config.api b/misk-config/api/misk-config.api index 1844ea2dca9..3f3eb619382 100644 --- a/misk-config/api/misk-config.api +++ b/misk-config/api/misk-config.api @@ -20,23 +20,23 @@ public final class misk/config/MiskConfig { public static final field INSTANCE Lmisk/config/MiskConfig; public static final fun filesInDir (Ljava/lang/String;Ljava/io/FilenameFilter;)Ljava/util/List; public static synthetic fun filesInDir$default (Ljava/lang/String;Ljava/io/FilenameFilter;ILjava/lang/Object;)Ljava/util/List; - public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lcom/fasterxml/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;)Lmisk/config/Config; - public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lcom/fasterxml/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;Z)Lmisk/config/Config; - public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lcom/fasterxml/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ZLcom/fasterxml/jackson/databind/deser/BeanDeserializerModifier;)Lmisk/config/Config; public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lmisk/resources/ResourceLoader;)Lmisk/config/Config; - public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lcom/fasterxml/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ILjava/lang/Object;)Lmisk/config/Config; - public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lcom/fasterxml/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ZILjava/lang/Object;)Lmisk/config/Config; - public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lcom/fasterxml/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ZLcom/fasterxml/jackson/databind/deser/BeanDeserializerModifier;ILjava/lang/Object;)Lmisk/config/Config; + public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Ltools/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;)Lmisk/config/Config; + public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Ltools/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;Z)Lmisk/config/Config; + public static final fun load (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Ltools/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ZLtools/jackson/databind/deser/ValueDeserializerModifier;)Lmisk/config/Config; public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lmisk/resources/ResourceLoader;ILjava/lang/Object;)Lmisk/config/Config; + public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Ltools/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ILjava/lang/Object;)Lmisk/config/Config; + public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Ltools/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ZILjava/lang/Object;)Lmisk/config/Config; + public static synthetic fun load$default (Ljava/lang/Class;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Ltools/jackson/databind/JsonNode;Lmisk/resources/ResourceLoader;ZLtools/jackson/databind/deser/ValueDeserializerModifier;ILjava/lang/Object;)Lmisk/config/Config; public final fun loadConfigYamlMap (Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lmisk/resources/ResourceLoader;)Ljava/util/Map; public static synthetic fun loadConfigYamlMap$default (Lmisk/config/MiskConfig;Ljava/lang/String;Lwisp/deployment/Deployment;Ljava/util/List;Lmisk/resources/ResourceLoader;ILjava/lang/Object;)Ljava/util/Map; public final fun toRedactedYaml (Lmisk/config/Config;Lmisk/resources/ResourceLoader;)Ljava/lang/String; } -public final class misk/config/MiskConfig$DeserializerModifierModule : com/fasterxml/jackson/databind/module/SimpleModule { - public fun (Lcom/fasterxml/jackson/databind/deser/BeanDeserializerModifier;)V - public final fun getDeserializerModifier ()Lcom/fasterxml/jackson/databind/deser/BeanDeserializerModifier; - public fun setupModule (Lcom/fasterxml/jackson/databind/Module$SetupContext;)V +public final class misk/config/MiskConfig$DeserializerModifierModule : tools/jackson/databind/module/SimpleModule { + public fun (Ltools/jackson/databind/deser/ValueDeserializerModifier;)V + public final fun getDeserializerModifier ()Ltools/jackson/databind/deser/ValueDeserializerModifier; + public fun setupModule (Ltools/jackson/databind/JacksonModule$SetupContext;)V } public final class misk/config/MiskConfig$RealSecret : misk/config/Secret { @@ -47,16 +47,17 @@ public final class misk/config/MiskConfig$RealSecret : misk/config/Secret { public fun toString ()Ljava/lang/String; } -public final class misk/config/MiskConfig$RedactSecretJacksonModule : com/fasterxml/jackson/databind/module/SimpleModule { +public final class misk/config/MiskConfig$RedactSecretJacksonModule : tools/jackson/databind/module/SimpleModule { public fun ()V - public fun setupModule (Lcom/fasterxml/jackson/databind/Module$SetupContext;)V + public fun setupModule (Ltools/jackson/databind/JacksonModule$SetupContext;)V } -public final class misk/config/MiskConfig$SecretJacksonModule : com/fasterxml/jackson/databind/module/SimpleModule { - public fun (Lmisk/resources/ResourceLoader;Lcom/fasterxml/jackson/databind/ObjectMapper;)V - public final fun getMapper ()Lcom/fasterxml/jackson/databind/ObjectMapper; +public final class misk/config/MiskConfig$SecretJacksonModule : tools/jackson/databind/module/SimpleModule { + public fun (Lmisk/resources/ResourceLoader;Lkotlin/jvm/functions/Function0;)V + public fun (Lmisk/resources/ResourceLoader;Ltools/jackson/databind/ObjectMapper;)V + public final fun getMapper ()Ltools/jackson/databind/ObjectMapper; public final fun getResourceLoader ()Lmisk/resources/ResourceLoader; - public fun setupModule (Lcom/fasterxml/jackson/databind/Module$SetupContext;)V + public fun setupModule (Ltools/jackson/databind/JacksonModule$SetupContext;)V } public abstract interface annotation class misk/config/Redact : java/lang/annotation/Annotation { diff --git a/misk-config/build.gradle.kts b/misk-config/build.gradle.kts index 864bda7d081..10e8b461fa7 100644 --- a/misk-config/build.gradle.kts +++ b/misk-config/build.gradle.kts @@ -20,7 +20,6 @@ dependencies { implementation(libs.guava) implementation(libs.jacksonCore) implementation(libs.jacksonDataformatYaml) - implementation(libs.jacksonJsr310) implementation(libs.jacksonKotlin) implementation(libs.loggingApi) implementation(libs.okio) diff --git a/misk-config/src/main/kotlin/misk/config/MiskConfig.kt b/misk-config/src/main/kotlin/misk/config/MiskConfig.kt index a116a13d17f..050eacea3ac 100644 --- a/misk-config/src/main/kotlin/misk/config/MiskConfig.kt +++ b/misk-config/src/main/kotlin/misk/config/MiskConfig.kt @@ -1,32 +1,6 @@ package misk.config import com.fasterxml.jackson.annotation.JacksonAnnotationsInside -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonToken -import com.fasterxml.jackson.databind.BeanProperty -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JavaType -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.ObjectMapper -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier -import com.fasterxml.jackson.databind.deser.ContextualDeserializer -import com.fasterxml.jackson.databind.exc.InvalidFormatException -import com.fasterxml.jackson.databind.exc.MismatchedInputException -import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.ser.ContextualSerializer -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.kotlin.KotlinInvalidNullException -import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.common.base.Joiner import java.io.File import java.io.FilenameFilter @@ -36,6 +10,32 @@ import kotlin.time.ExperimentalTime import misk.logging.getLogger import misk.resources.ResourceLoader import org.apache.commons.lang3.StringUtils +import tools.jackson.core.JsonGenerator +import tools.jackson.core.JsonParser +import tools.jackson.core.JsonToken +import tools.jackson.databind.BeanProperty +import tools.jackson.databind.DatabindException +import tools.jackson.databind.DeserializationContext +import tools.jackson.databind.DeserializationFeature +import tools.jackson.databind.JavaType +import tools.jackson.databind.JsonNode +import tools.jackson.databind.MapperFeature +import tools.jackson.databind.ObjectMapper +import tools.jackson.databind.SerializationContext +import tools.jackson.databind.ValueDeserializer +import tools.jackson.databind.ValueSerializer +import tools.jackson.databind.annotation.JsonSerialize +import tools.jackson.databind.cfg.EnumFeature +import tools.jackson.databind.deser.ValueDeserializerModifier +import tools.jackson.databind.exc.InvalidFormatException +import tools.jackson.databind.exc.MismatchedInputException +import tools.jackson.databind.exc.UnrecognizedPropertyException +import tools.jackson.databind.module.SimpleModule +import tools.jackson.databind.node.ObjectNode +import tools.jackson.dataformat.yaml.YAMLMapper +import tools.jackson.module.kotlin.KotlinFeature +import tools.jackson.module.kotlin.KotlinInvalidNullException +import tools.jackson.module.kotlin.KotlinModule import wisp.deployment.Deployment object MiskConfig { @@ -125,12 +125,10 @@ object MiskConfig { overrideValues: JsonNode? = null, resourceLoader: ResourceLoader = ResourceLoader.SYSTEM, failOnUnknownProperties: Boolean, - deserializerModifier: BeanDeserializerModifier? = null, + deserializerModifier: ValueDeserializerModifier? = null, ): T { check(!Secret::class.java.isAssignableFrom(configClass)) { "Top level service config cannot be a Secret<*>" } - val mapper = newObjectMapper(resourceLoader, false, deserializerModifier) - val configYamls = loadConfigYamlMap(appName, deployment, overrideResources, resourceLoader) check(configYamls.values.any { it != null }) { "could not find configuration files - checked ${configYamls.keys}" } @@ -139,7 +137,7 @@ object MiskConfig { val configFile = "$appName-${configEnvironmentName.lowercase(Locale.US)}.yaml" return readFlattenedYaml( - mapper, + { failOnUnknown -> newObjectMapper(resourceLoader, false, deserializerModifier, failOnUnknown) }, jsonNode, configClass, configFile, @@ -150,7 +148,7 @@ object MiskConfig { } private fun readFlattenedYaml( - mapper: ObjectMapper, + newMapper: (failOnUnknownProperties: Boolean) -> ObjectMapper, jsonNode: JsonNode, configClass: Class, configFile: String, @@ -160,24 +158,32 @@ object MiskConfig { ): T { try { @Suppress("UNCHECKED_CAST") - return mapper.readValue(jsonNode.toString(), configClass) as T + return newMapper(true).readValue(jsonNode.toString(), configClass) as T } catch (e: UnrecognizedPropertyException) { if (failOnUnknownProperties) { throw IllegalStateException("failed to load configuration for $appName $configEnvironmentName: ${e.message}", e) } - val path = Joiner.on('.').join(e.path.map { it.fieldName ?: it.index }) + val path = Joiner.on('.').join(e.path.map { it.propertyName ?: it.index }) logger.warn(e) { "$configFile: '$path' not found in '${configClass.simpleName}', ignoring " + suggestSpelling(e) } - // Try again, this time ignoring unknown properties. - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - return readFlattenedYaml(mapper, jsonNode, configClass, configFile, appName, configEnvironmentName, false) + // Try again, this time ignoring unknown properties. Mappers are immutable, so build a new one + // rather than reconfiguring this one. + return readFlattenedYaml( + { newMapper(false) }, + jsonNode, + configClass, + configFile, + appName, + configEnvironmentName, + false, + ) } catch (e: KotlinInvalidNullException) { throwMissingPropertyException(e, configClass, configFile, jsonNode) } catch (e: InvalidFormatException) { // The property is present, it just cannot be represented as the declared type. Reporting it as missing (which // the MismatchedInputException branch below would do) sends readers looking for the wrong problem. - val path = Joiner.on('.').join(e.path.map { it.fieldName ?: it.index }) + val path = Joiner.on('.').join(e.path.map { it.propertyName ?: it.index }) throw IllegalStateException( "failed to load configuration for $appName $configEnvironmentName:" + " could not parse '$path' in $configFile: ${e.originalMessage}", @@ -191,12 +197,12 @@ object MiskConfig { } private fun throwMissingPropertyException( - e: JsonMappingException, + e: DatabindException, configClass: Class, configFile: String, jsonNode: JsonNode, ): Nothing { - val path = Joiner.on('.').join(e.path.map { it.fieldName ?: it.index }) + val path = Joiner.on('.').join(e.path.map { it.propertyName ?: it.index }) throw IllegalStateException( "could not find '${path}' of '${configClass.simpleName}'" + " in $configFile or in any of the combined logical config " + @@ -209,11 +215,11 @@ object MiskConfig { if (jsonNode.isObject) { val objectNode = jsonNode as ObjectNode - var seq = objectNode.fieldNames().asSequence().map { Joiner.on('.').join(pathPrefix, it) } + var seq = objectNode.propertyNames().asSequence().map { Joiner.on('.').join(pathPrefix, it) } // Recursively add the field names of any object fields. seq += - objectNode.fields().asSequence().flatMap { + objectNode.properties().asSequence().flatMap { val nextPrefix = Joiner.on('.').join(pathPrefix, it.key) allFieldNames(it.value, nextPrefix) } @@ -240,37 +246,59 @@ object MiskConfig { } fun toRedactedYaml(config: T, resourceLoader: ResourceLoader): String { - val serializingMapper = newObjectMapper(resourceLoader, true, null) + val serializingMapper = newObjectMapper(resourceLoader, true, null, failOnUnknownProperties = true) return serializingMapper.writeValueAsString(config) } private fun newObjectMapper( resourceLoader: ResourceLoader, redactSecrets: Boolean, - deserializerModifier: BeanDeserializerModifier?, + deserializerModifier: ValueDeserializerModifier?, + failOnUnknownProperties: Boolean, ): ObjectMapper { - val mapper = ObjectMapper(YAMLFactory()).registerModules(KotlinModule.Builder().build(), JavaTimeModule()) - - // Fail on null ints/doubles. - mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true) + // The secret and resource deserializers parse nested documents with the very mapper they are + // registered on. Mappers are immutable and built in one shot, so hand the modules a supplier + // that resolves once the build below completes. + lateinit var mapper: ObjectMapper + val mapperProvider = { mapper } + + val builder = + YAMLMapper.builder() + // StrictNullChecks defaults off in Jackson 2 and on in Jackson 3. Leaving it on rejects a null element of a + // collection nested inside a map even when that element type is declared nullable -- Map> + // fails while a top-level Set is accepted -- so existing config stops loading. Keep it off to match + // Jackson 2. + .addModule(KotlinModule.Builder().disable(KotlinFeature.StrictNullChecks).build()) + // Fail on null ints/doubles. + .enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) + // Jackson 3 defaults this off. Config files are hand-written and a typo'd property should + // still surface as the "did you mean" warning below, so keep the Jackson 2 behaviour. + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, failOnUnknownProperties) + // Jackson 3 defaults this on. Redacted config is rendered in the dashboard in declaration + // order today; sorting would silently reshuffle every service's config page. + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + // Jackson 3 defaults these on. Config enums are matched by name, and toString() is + // frequently overridden for display, so switching would break existing config files. + .disable(EnumFeature.READ_ENUMS_USING_TO_STRING, EnumFeature.WRITE_ENUMS_USING_TO_STRING) // The SecretDeserializer supports deserializing json, so bind last so it can use previous // mappings. if (redactSecrets) { - mapper.registerModule(RedactSecretJacksonModule()) + builder.addModule(RedactSecretJacksonModule()) } else { - mapper.registerModule(SecretJacksonModule(resourceLoader, mapper)) + builder.addModule(SecretJacksonModule(resourceLoader, mapperProvider)) } // The ResourceAwareDeserializer lets string and other primitive types be loaded by reference using resource loader // paths (classpath, filesystem, environment...) without using the Secret type. // This is useful for non-sensitive data or using environment variables to pass data into non-Secret types in // existing config or framework provided config classes. - mapper.registerModule(ResourceAwareJacksonModule(resourceLoader, mapper)) + builder.addModule(ResourceAwareJacksonModule(resourceLoader, mapperProvider)) // The deserializerModifier can be null if this mapper is serializing only. - deserializerModifier?.let { mapper.registerModule(DeserializerModifierModule(it)) } + deserializerModifier?.let { builder.addModule(DeserializerModifierModule(it)) } + mapper = builder.build() return mapper } @@ -289,7 +317,7 @@ object MiskConfig { * Returns a JsonNode that combines the YAMLs in `configYamls`. If two nodes define the same value the last one wins. */ private fun flattenYamlMap(configYamls: Map, overrideValues: JsonNode?): JsonNode { - val mapper = ObjectMapper(YAMLFactory()).registerModules(KotlinModule.Builder().build(), JavaTimeModule()) + val mapper = YAMLMapper.builder().addModule(KotlinModule.Builder().build()).build() var result = mapper.createObjectNode() for ((key, value) in configYamls) { @@ -328,35 +356,51 @@ object MiskConfig { private fun embeddedConfigFileNames(appName: String, deployment: Deployment) = listOf("common", deployment.mapToEnvironmentName().lowercase(Locale.US)).map { "$appName-$it.yaml" } - class SecretJacksonModule(val resourceLoader: ResourceLoader, val mapper: ObjectMapper) : SimpleModule() { + class SecretJacksonModule(val resourceLoader: ResourceLoader, private val mapperProvider: () -> ObjectMapper) : + SimpleModule() { + constructor(resourceLoader: ResourceLoader, mapper: ObjectMapper) : this(resourceLoader, { mapper }) + + /** + * The mapper nested secret documents are parsed with. Resolved lazily because a module has to be registered before + * the mapper it belongs to exists. + */ + val mapper: ObjectMapper + get() = mapperProvider() + override fun setupModule(context: SetupContext?) { - addDeserializer(Secret::class.java, SecretDeserializer(resourceLoader, mapper)) + addDeserializer(Secret::class.java, SecretDeserializer(resourceLoader, mapperProvider)) super.setupModule(context) } } - class DeserializerModifierModule(val deserializerModifier: BeanDeserializerModifier) : SimpleModule() { + class DeserializerModifierModule(val deserializerModifier: ValueDeserializerModifier) : SimpleModule() { override fun setupModule(context: SetupContext?) { setDeserializerModifier(deserializerModifier) super.setupModule(context) } } - private class ResourceAwareJacksonModule(val resourceLoader: ResourceLoader, val mapper: ObjectMapper) : + private class ResourceAwareJacksonModule(val resourceLoader: ResourceLoader, val mapperProvider: () -> ObjectMapper) : SimpleModule() { override fun setupModule(context: SetupContext?) { - addDeserializer(String::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(Int::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(Integer::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(Long::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(java.lang.Long::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(Float::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(java.lang.Float::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) - addDeserializer(Boolean::class.java, ResourceAwareDeserializer(resourceLoader, mapper)) + addDeserializer(String::class.java, ResourceAwareDeserializer(resourceLoader, mapperProvider)) + addDeserializer(Int::class.java, ResourceAwareDeserializer(resourceLoader, mapperProvider)) + addDeserializer(Integer::class.java, ResourceAwareDeserializer(resourceLoader, mapperProvider)) + addDeserializer(Long::class.java, ResourceAwareDeserializer(resourceLoader, mapperProvider)) + addDeserializer( + java.lang.Long::class.java, + ResourceAwareDeserializer(resourceLoader, mapperProvider), + ) + addDeserializer(Float::class.java, ResourceAwareDeserializer(resourceLoader, mapperProvider)) + addDeserializer( + java.lang.Float::class.java, + ResourceAwareDeserializer(resourceLoader, mapperProvider), + ) + addDeserializer(Boolean::class.java, ResourceAwareDeserializer(resourceLoader, mapperProvider)) addDeserializer( java.lang.Boolean::class.java, - ResourceAwareDeserializer(resourceLoader, mapper), + ResourceAwareDeserializer(resourceLoader, mapperProvider), ) super.setupModule(context) @@ -365,19 +409,19 @@ object MiskConfig { private inline fun ResourceAwareDeserializer( resourceLoader: ResourceLoader, - mapper: ObjectMapper, - ): ResourceAwareDeserializer = ResourceAwareDeserializer(T::class, resourceLoader, mapper) + noinline mapperProvider: () -> ObjectMapper, + ): ResourceAwareDeserializer = ResourceAwareDeserializer(T::class, resourceLoader, mapperProvider) private class ResourceAwareDeserializer( val typeClass: KClass, val resourceLoader: ResourceLoader, - val mapper: ObjectMapper, + val mapperProvider: () -> ObjectMapper, val type: JavaType? = null, - ) : JsonDeserializer(), ContextualDeserializer { + ) : ValueDeserializer() { override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): T? { if (type == null) { // This only happens if ObjectMapper does not call createContextual for this property. - throw JsonMappingException.from(jsonParser, "Attempting to deserialize an object with no type") + throw DatabindException.from(jsonParser, "Attempting to deserialize an object with no type") } val maybeReferenceWithMarkers = jsonParser.valueAsString @@ -425,7 +469,7 @@ object MiskConfig { val maybeReference = "$scheme:$path" - resourceLoader.loadResource(maybeReference, type, mapper, default) as? T? + resourceLoader.loadResource(maybeReference, type, mapperProvider(), default) as? T? } // Not a resource reference, so convert the scalar itself. This is deliberately evaluated here rather than up // front: a reference like "${environment:PORT}" is not a valid Int, so converting eagerly would reject @@ -433,40 +477,41 @@ object MiskConfig { ?: jsonParser.valueAsTypeOrNull(type) as? T? } - override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*>? { - return ResourceAwareDeserializer(typeClass, resourceLoader, mapper, mapper.constructType(typeClass.java)) + override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): ValueDeserializer<*> { + val resolved = ctxt.constructType(typeClass.java) + return ResourceAwareDeserializer(typeClass, resourceLoader, mapperProvider, resolved) } } private class SecretDeserializer( val resourceLoader: ResourceLoader, - val mapper: ObjectMapper, + val mapperProvider: () -> ObjectMapper, val type: JavaType? = null, - ) : JsonDeserializer>(), ContextualDeserializer { + ) : ValueDeserializer>() { override fun createContextual( - deserializationContext: DeserializationContext?, + deserializationContext: DeserializationContext, property: BeanProperty, - ): JsonDeserializer<*> { - return SecretDeserializer(resourceLoader, mapper, property.type.bindings.getBoundType(0)) + ): ValueDeserializer<*> { + return SecretDeserializer(resourceLoader, mapperProvider, property.type.bindings.getBoundType(0)) } override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): Secret<*>? { if (type == null) { // This only happens if ObjectMapper does not call createContextual for this property. - throw JsonMappingException.from(jsonParser, "Attempting to deserialize an object with no type") + throw DatabindException.from(jsonParser, "Attempting to deserialize an object with no type") } val reference = jsonParser.valueAsString - return RealSecret(resourceLoader.loadResource(reference, type, mapper), reference) + return RealSecret(resourceLoader.loadResource(reference, type, mapperProvider()), reference) } } - internal class RedactSecretJsonSerializer : JsonSerializer(), ContextualSerializer { - override fun serialize(value: Any, gen: JsonGenerator, serializers: SerializerProvider) { + internal class RedactSecretJsonSerializer : ValueSerializer() { + override fun serialize(value: Any, gen: JsonGenerator, ctxt: SerializationContext) { gen.writeString("████████") } - override fun createContextual(prov: SerializerProvider, property: BeanProperty): JsonSerializer<*> { + override fun createContextual(ctxt: SerializationContext, property: BeanProperty): ValueSerializer<*> { return RedactSecretJsonSerializer() } } @@ -478,8 +523,8 @@ object MiskConfig { } } - private class RedactSecretSerializer : JsonSerializer>(), ContextualSerializer { - override fun serialize(value: Secret<*>, gen: JsonGenerator, serializers: SerializerProvider?) { + private class RedactSecretSerializer : ValueSerializer>() { + override fun serialize(value: Secret<*>, gen: JsonGenerator, ctxt: SerializationContext) { if ((value as? RealSecret<*>)?.reference?.isNotBlank() == true) { gen.writeString("${value.reference} -> ████████") } else { @@ -487,7 +532,7 @@ object MiskConfig { } } - override fun createContextual(prov: SerializerProvider?, property: BeanProperty): JsonSerializer<*> { + override fun createContextual(ctxt: SerializationContext, property: BeanProperty): ValueSerializer<*> { return RedactSecretSerializer() } } diff --git a/misk-config/src/test/kotlin/misk/config/MiskConfigTest.kt b/misk-config/src/test/kotlin/misk/config/MiskConfigTest.kt index 17bd1cf05fb..8a857aee26d 100644 --- a/misk-config/src/test/kotlin/misk/config/MiskConfigTest.kt +++ b/misk-config/src/test/kotlin/misk/config/MiskConfigTest.kt @@ -1,6 +1,5 @@ package misk.config -import com.fasterxml.jackson.databind.ObjectMapper import com.google.inject.util.Modules import jakarta.inject.Inject import java.io.File @@ -24,6 +23,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.slf4j.event.Level +import tools.jackson.databind.ObjectMapper import uk.org.webcompere.systemstubs.environment.EnvironmentVariables import uk.org.webcompere.systemstubs.jupiter.SystemStub import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension @@ -269,7 +269,7 @@ class MiskConfigTest { assertFailsWith { MiskConfig.load(TestConfig::class.java, "unknownproperty", TESTING, failOnUnknownProperties = true) } - assertThat(exception).hasMessageContaining("Unrecognized field \"blue_items\"") + assertThat(exception).hasMessageContaining("Unrecognized property \"blue_items\"") } @Test diff --git a/misk-testing/src/main/kotlin/org/assertj/core/api/AssertExtensions.kt b/misk-testing/src/main/kotlin/org/assertj/core/api/AssertExtensions.kt index cd8a17f04dc..b2e04e927bd 100644 --- a/misk-testing/src/main/kotlin/org/assertj/core/api/AssertExtensions.kt +++ b/misk-testing/src/main/kotlin/org/assertj/core/api/AssertExtensions.kt @@ -1,10 +1,10 @@ package org.assertj.core.api -import com.fasterxml.jackson.core.JacksonException -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.ObjectMapper import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.AssertionsForClassTypes.fail +import tools.jackson.core.JacksonException +import tools.jackson.core.StreamReadFeature +import tools.jackson.databind.json.JsonMapper inline fun MapAssert.containsExactly( vararg p: Pair @@ -12,7 +12,7 @@ inline fun MapAssert.containsExactly( return isEqualTo(mapOf(*p)) } -private val objectMapper = ObjectMapper().configure(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION, true) +private val objectMapper = JsonMapper.builder().enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION).build() fun AbstractCharSequenceAssert<*, ACTUAL>.isEqualToAsJson( expected: CharSequence diff --git a/misk/api/misk.api b/misk/api/misk.api index 444d456c0d8..7047dc03c38 100644 --- a/misk/api/misk.api +++ b/misk/api/misk.api @@ -119,12 +119,12 @@ public final class misk/client/BackwardsCompatibleClientsConfig { public fun toString ()Ljava/lang/String; } -public final class misk/client/BackwardsCompatibleClientsConfigConverter : com/fasterxml/jackson/databind/util/Converter { +public final class misk/client/BackwardsCompatibleClientsConfigConverter : tools/jackson/databind/util/StdConverter { public fun ()V public synthetic fun convert (Ljava/lang/Object;)Ljava/lang/Object; public fun convert (Lmisk/client/BackwardsCompatibleClientsConfig;)Lmisk/client/HttpClientsConfig; - public fun getInputType (Lcom/fasterxml/jackson/databind/type/TypeFactory;)Lcom/fasterxml/jackson/databind/JavaType; - public fun getOutputType (Lcom/fasterxml/jackson/databind/type/TypeFactory;)Lcom/fasterxml/jackson/databind/JavaType; + public fun getInputType (Ltools/jackson/databind/type/TypeFactory;)Ltools/jackson/databind/JavaType; + public fun getOutputType (Ltools/jackson/databind/type/TypeFactory;)Ltools/jackson/databind/JavaType; } public final class misk/client/BackwardsCompatibleEndpointConfig { diff --git a/misk/src/main/kotlin/misk/client/HttpClientsConfig.kt b/misk/src/main/kotlin/misk/client/HttpClientsConfig.kt index cca3370e7b2..b171ee12907 100644 --- a/misk/src/main/kotlin/misk/client/HttpClientsConfig.kt +++ b/misk/src/main/kotlin/misk/client/HttpClientsConfig.kt @@ -1,13 +1,13 @@ package misk.client import com.fasterxml.jackson.annotation.JsonAlias -import com.fasterxml.jackson.databind.annotation.JsonDeserialize import java.net.URL import java.time.Duration import misk.config.Config import misk.logging.getLogger import misk.security.ssl.CertStoreConfig import misk.security.ssl.TrustStoreConfig +import tools.jackson.databind.annotation.JsonDeserialize @JsonDeserialize(converter = BackwardsCompatibleClientsConfigConverter::class) data class HttpClientsConfig diff --git a/misk/src/main/kotlin/misk/client/HttpClientsConfigBackwardsCompatibility.kt b/misk/src/main/kotlin/misk/client/HttpClientsConfigBackwardsCompatibility.kt index 94b6126251f..fe14736dee5 100644 --- a/misk/src/main/kotlin/misk/client/HttpClientsConfigBackwardsCompatibility.kt +++ b/misk/src/main/kotlin/misk/client/HttpClientsConfigBackwardsCompatibility.kt @@ -1,9 +1,9 @@ package misk.client import com.fasterxml.jackson.annotation.JsonAlias -import com.fasterxml.jackson.databind.type.TypeFactory -import com.fasterxml.jackson.databind.util.Converter import java.time.Duration +import tools.jackson.databind.type.TypeFactory +import tools.jackson.databind.util.StdConverter data class BackwardsCompatibleEndpointConfig @JvmOverloads @@ -47,7 +47,7 @@ constructor( val logRequests: Boolean = false, ) -class BackwardsCompatibleClientsConfigConverter : Converter { +class BackwardsCompatibleClientsConfigConverter : StdConverter() { override fun getInputType(typeFactory: TypeFactory) = typeFactory.constructType(BackwardsCompatibleClientsConfig::class.java)