From 258835a0cba19b51608d5ed37c56bb42aff11c05 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Thu, 12 Feb 2026 13:18:47 +0000 Subject: [PATCH 1/2] Move ObjectMapper to internal YamlObjectMapper class. Spring starter needs same ObjectMapper config but can't access package-private field without reflection. New public internal class provides access. Fixes #7843 --- .../fileconfig/DeclarativeConfiguration.java | 32 ++++--------- .../fileconfig/internal/YamlObjectMapper.java | 46 +++++++++++++++++++ 2 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java index 6e67bd0bdb4..6fceb43ec43 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java @@ -5,10 +5,7 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.common.ComponentLoader; @@ -16,6 +13,7 @@ import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener; import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.YamlObjectMapper; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SamplerModel; import io.opentelemetry.sdk.internal.ExtendedOpenTelemetrySdk; @@ -62,22 +60,6 @@ public final class DeclarativeConfiguration { private static final ComponentLoader DEFAULT_COMPONENT_LOADER = ComponentLoader.forClassLoader(DeclarativeConfigProperties.class.getClassLoader()); - // Visible for testing - static final ObjectMapper MAPPER; - - static { - MAPPER = - new ObjectMapper() - // Create empty object instances for keys which are present but have null values - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); - // Boxed primitives which are present but have null values should be set to null, rather than - // empty instances - MAPPER.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - MAPPER.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - MAPPER.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - MAPPER.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - } - private DeclarativeConfiguration() {} /** @@ -158,7 +140,8 @@ public static OpenTelemetryConfigurationModel parse(InputStream configuration) { static OpenTelemetryConfigurationModel parse( InputStream configuration, Map environmentVariables) { Object yamlObj = loadYaml(configuration, environmentVariables); - return MAPPER.convertValue(yamlObj, OpenTelemetryConfigurationModel.class); + return YamlObjectMapper.getInstance() + .convertValue(yamlObj, OpenTelemetryConfigurationModel.class); } // Visible for testing @@ -192,7 +175,8 @@ public static DeclarativeConfigProperties toConfigProperties(InputStream configu static DeclarativeConfigProperties toConfigProperties( Object model, ComponentLoader componentLoader) { Map configurationMap = - MAPPER.convertValue(model, new TypeReference>() {}); + YamlObjectMapper.getInstance() + .convertValue(model, new TypeReference>() {}); if (configurationMap == null) { configurationMap = Collections.emptyMap(); } @@ -213,8 +197,10 @@ public static Sampler createSampler(DeclarativeConfigProperties genericSamplerMo YamlDeclarativeConfigProperties yamlDeclarativeConfigProperties = requireYamlDeclarativeConfigProperties(genericSamplerModel); SamplerModel samplerModel = - MAPPER.convertValue( - DeclarativeConfigProperties.toMap(yamlDeclarativeConfigProperties), SamplerModel.class); + YamlObjectMapper.getInstance() + .convertValue( + DeclarativeConfigProperties.toMap(yamlDeclarativeConfigProperties), + SamplerModel.class); return createAndMaybeCleanup( SamplerFactory.getInstance(), DeclarativeConfigContext.create(yamlDeclarativeConfigProperties.getComponentLoader()), diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java new file mode 100644 index 00000000000..a14dc53174f --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig.internal; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Provides a configured {@link ObjectMapper} for YAML declarative configuration parsing. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class YamlObjectMapper { + + private static final ObjectMapper INSTANCE; + + static { + INSTANCE = + new ObjectMapper() + // Create empty object instances for keys which are present but have null values + .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); + // Boxed primitives which are present but have null values should be set to null, rather than + // empty instances + INSTANCE.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + INSTANCE.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + INSTANCE.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + INSTANCE.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + } + + private YamlObjectMapper() {} + + /** + * Returns the configured {@link ObjectMapper} instance for parsing YAML declarative + * configuration. + * + * @return the configured ObjectMapper + */ + public static ObjectMapper getInstance() { + return INSTANCE; + } +} From c8729574e1754800c2b090415ac25e24247c71ae Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Mon, 16 Feb 2026 12:10:14 +0000 Subject: [PATCH 2/2] Revert ObjectMapper to DeclarativeConfiguration with copy-paste docs Per maintainer feedback, avoid exposing ObjectMapper API (internal or public). Instead, document configuration for copy-paste approach. - Move ObjectMapper back from YamlObjectMapper to DeclarativeConfiguration - Add "For Implementers" section in javadoc with setup example - Add field javadoc explaining config and referencing class docs - Remove YamlObjectMapper.java --- .../fileconfig/DeclarativeConfiguration.java | 70 ++++++++++++++++--- .../fileconfig/internal/YamlObjectMapper.java | 46 ------------ 2 files changed, 61 insertions(+), 55 deletions(-) delete mode 100644 sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java index 6fceb43ec43..4e6fa303825 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java @@ -5,7 +5,10 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.common.ComponentLoader; @@ -13,7 +16,6 @@ import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener; import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; -import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.YamlObjectMapper; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SamplerModel; import io.opentelemetry.sdk.internal.ExtendedOpenTelemetrySdk; @@ -51,6 +53,31 @@ * YAML * configuration file. + * + *

For Implementers

+ * + *

External consumers needing to parse OpenTelemetry YAML configuration files should use the same + * Jackson ObjectMapper configuration for compatibility. This configuration is intentionally not + * exposed as API to avoid coupling. Instead, copy the following setup: + * + *

{@code
+ * ObjectMapper mapper = new ObjectMapper()
+ *     // Create empty object instances for keys which are present but have null values
+ *     .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
+ * // Boxed primitives which are present but have null values should be set to null,
+ * // rather than empty instances
+ * mapper.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
+ * mapper.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
+ * mapper.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
+ * mapper.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
+ * }
+ * + *

Why this configuration: + * + *

    + *
  • Default behavior creates empty objects for null values to match YAML schema expectations + *
  • Boxed primitives remain null to distinguish between absent and explicitly null values + *
*/ public final class DeclarativeConfiguration { @@ -60,6 +87,35 @@ public final class DeclarativeConfiguration { private static final ComponentLoader DEFAULT_COMPONENT_LOADER = ComponentLoader.forClassLoader(DeclarativeConfigProperties.class.getClassLoader()); + /** + * ObjectMapper configured for YAML declarative configuration parsing. + * + *

Configuration: + * + *

    + *
  • Default: Creates empty objects for present keys with null values + *
  • Boxed primitives (String, Integer, Double, Boolean): Remain null when null + *
+ * + *

External consumers needing compatible parsing should copy this configuration. See class + * javadoc for details and code example. + */ + // Visible for testing + static final ObjectMapper MAPPER; + + static { + MAPPER = + new ObjectMapper() + // Create empty object instances for keys which are present but have null values + .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); + // Boxed primitives which are present but have null values should be set to null, rather than + // empty instances + MAPPER.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + MAPPER.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + MAPPER.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + MAPPER.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); + } + private DeclarativeConfiguration() {} /** @@ -140,8 +196,7 @@ public static OpenTelemetryConfigurationModel parse(InputStream configuration) { static OpenTelemetryConfigurationModel parse( InputStream configuration, Map environmentVariables) { Object yamlObj = loadYaml(configuration, environmentVariables); - return YamlObjectMapper.getInstance() - .convertValue(yamlObj, OpenTelemetryConfigurationModel.class); + return MAPPER.convertValue(yamlObj, OpenTelemetryConfigurationModel.class); } // Visible for testing @@ -175,8 +230,7 @@ public static DeclarativeConfigProperties toConfigProperties(InputStream configu static DeclarativeConfigProperties toConfigProperties( Object model, ComponentLoader componentLoader) { Map configurationMap = - YamlObjectMapper.getInstance() - .convertValue(model, new TypeReference>() {}); + MAPPER.convertValue(model, new TypeReference>() {}); if (configurationMap == null) { configurationMap = Collections.emptyMap(); } @@ -197,10 +251,8 @@ public static Sampler createSampler(DeclarativeConfigProperties genericSamplerMo YamlDeclarativeConfigProperties yamlDeclarativeConfigProperties = requireYamlDeclarativeConfigProperties(genericSamplerModel); SamplerModel samplerModel = - YamlObjectMapper.getInstance() - .convertValue( - DeclarativeConfigProperties.toMap(yamlDeclarativeConfigProperties), - SamplerModel.class); + MAPPER.convertValue( + DeclarativeConfigProperties.toMap(yamlDeclarativeConfigProperties), SamplerModel.class); return createAndMaybeCleanup( SamplerFactory.getInstance(), DeclarativeConfigContext.create(yamlDeclarativeConfigProperties.getComponentLoader()), diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java deleted file mode 100644 index a14dc53174f..00000000000 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/internal/YamlObjectMapper.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.sdk.extension.incubator.fileconfig.internal; - -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Provides a configured {@link ObjectMapper} for YAML declarative configuration parsing. - * - *

This class is internal and is hence not for public use. Its APIs are unstable and can change - * at any time. - */ -public final class YamlObjectMapper { - - private static final ObjectMapper INSTANCE; - - static { - INSTANCE = - new ObjectMapper() - // Create empty object instances for keys which are present but have null values - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); - // Boxed primitives which are present but have null values should be set to null, rather than - // empty instances - INSTANCE.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - INSTANCE.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - INSTANCE.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - INSTANCE.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); - } - - private YamlObjectMapper() {} - - /** - * Returns the configured {@link ObjectMapper} instance for parsing YAML declarative - * configuration. - * - * @return the configured ObjectMapper - */ - public static ObjectMapper getInstance() { - return INSTANCE; - } -}