diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4fc948e84ff..5a836cb4d0e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,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" @@ -121,12 +121,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 108429a652f..52ff612ff50 100644 --- a/misk-config/src/main/kotlin/misk/config/MiskConfig.kt +++ b/misk-config/src/main/kotlin/misk/config/MiskConfig.kt @@ -1,30 +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.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.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 @@ -34,6 +10,29 @@ 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.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.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.KotlinInvalidNullException +import tools.jackson.module.kotlin.KotlinModule import wisp.deployment.Deployment object MiskConfig { @@ -123,12 +122,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}" } @@ -137,7 +134,7 @@ object MiskConfig { val configFile = "$appName-${configEnvironmentName.lowercase(Locale.US)}.yaml" return readFlattenedYaml( - mapper, + { failOnUnknown -> newObjectMapper(resourceLoader, false, deserializerModifier, failOnUnknown) }, jsonNode, configClass, configFile, @@ -148,7 +145,7 @@ object MiskConfig { } private fun readFlattenedYaml( - mapper: ObjectMapper, + newMapper: (failOnUnknownProperties: Boolean) -> ObjectMapper, jsonNode: JsonNode, configClass: Class, configFile: String, @@ -158,18 +155,26 @@ 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: MismatchedInputException) { @@ -180,12 +185,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 " + @@ -198,11 +203,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) } @@ -229,37 +234,55 @@ 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() + .addModule(KotlinModule.Builder().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 } @@ -278,7 +301,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) { @@ -317,35 +340,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) @@ -354,19 +393,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 valueAsType = jsonParser.valueAsTypeOrNull(type) @@ -415,44 +454,45 @@ object MiskConfig { val maybeReference = "$scheme:$path" - resourceLoader.loadResource(maybeReference, type, mapper, default) as? T? + resourceLoader.loadResource(maybeReference, type, mapperProvider(), default) as? T? } ?: valueAsType as? T? // Not a resource reference, return the type as is. } - 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() } } @@ -464,8 +504,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 { @@ -473,7 +513,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 f99c697b122..921f900312c 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 @@ -229,7 +229,8 @@ class MiskConfigTest { assertFailsWith { MiskConfig.load(TestConfig::class.java, "unknownproperty", TESTING, failOnUnknownProperties = true) } - assertThat(exception).hasMessageContaining("Unrecognized field \"blue_items\"") + // Jackson 3 renamed "field" to "property" throughout its diagnostics. + 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)