55
66package io .opentelemetry .sdk .extension .incubator .fileconfig ;
77
8+ import com .fasterxml .jackson .annotation .JsonSetter ;
9+ import com .fasterxml .jackson .annotation .Nulls ;
810import com .fasterxml .jackson .core .type .TypeReference ;
11+ import com .fasterxml .jackson .databind .ObjectMapper ;
912import io .opentelemetry .api .incubator .config .DeclarativeConfigException ;
1013import io .opentelemetry .api .incubator .config .DeclarativeConfigProperties ;
1114import io .opentelemetry .common .ComponentLoader ;
1215import io .opentelemetry .sdk .OpenTelemetrySdk ;
1316import io .opentelemetry .sdk .autoconfigure .internal .SpiHelper ;
1417import io .opentelemetry .sdk .autoconfigure .spi .internal .AutoConfigureListener ;
1518import io .opentelemetry .sdk .autoconfigure .spi .internal .ComponentProvider ;
16- import io .opentelemetry .sdk .extension .incubator .fileconfig .internal .YamlObjectMapper ;
1719import io .opentelemetry .sdk .extension .incubator .fileconfig .internal .model .OpenTelemetryConfigurationModel ;
1820import io .opentelemetry .sdk .extension .incubator .fileconfig .internal .model .SamplerModel ;
1921import io .opentelemetry .sdk .internal .ExtendedOpenTelemetrySdk ;
5153 * <a
5254 * href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/data-model.md#yaml-file-format">YAML
5355 * configuration file</a>.
56+ *
57+ * <h2>For Implementers</h2>
58+ *
59+ * <p>External consumers needing to parse OpenTelemetry YAML configuration files should use the same
60+ * Jackson ObjectMapper configuration for compatibility. This configuration is intentionally not
61+ * exposed as API to avoid coupling. Instead, copy the following setup:
62+ *
63+ * <pre>{@code
64+ * ObjectMapper mapper = new ObjectMapper()
65+ * // Create empty object instances for keys which are present but have null values
66+ * .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
67+ * // Boxed primitives which are present but have null values should be set to null,
68+ * // rather than empty instances
69+ * mapper.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
70+ * mapper.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
71+ * mapper.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
72+ * mapper.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET));
73+ * }</pre>
74+ *
75+ * <p><b>Why this configuration:</b>
76+ *
77+ * <ul>
78+ * <li>Default behavior creates empty objects for null values to match YAML schema expectations
79+ * <li>Boxed primitives remain null to distinguish between absent and explicitly null values
80+ * </ul>
5481 */
5582public final class DeclarativeConfiguration {
5683
@@ -60,6 +87,35 @@ public final class DeclarativeConfiguration {
6087 private static final ComponentLoader DEFAULT_COMPONENT_LOADER =
6188 ComponentLoader .forClassLoader (DeclarativeConfigProperties .class .getClassLoader ());
6289
90+ /**
91+ * ObjectMapper configured for YAML declarative configuration parsing.
92+ *
93+ * <p>Configuration:
94+ *
95+ * <ul>
96+ * <li>Default: Creates empty objects for present keys with null values
97+ * <li>Boxed primitives (String, Integer, Double, Boolean): Remain null when null
98+ * </ul>
99+ *
100+ * <p>External consumers needing compatible parsing should copy this configuration. See class
101+ * javadoc for details and code example.
102+ */
103+ // Visible for testing
104+ static final ObjectMapper MAPPER ;
105+
106+ static {
107+ MAPPER =
108+ new ObjectMapper ()
109+ // Create empty object instances for keys which are present but have null values
110+ .setDefaultSetterInfo (JsonSetter .Value .forValueNulls (Nulls .AS_EMPTY ));
111+ // Boxed primitives which are present but have null values should be set to null, rather than
112+ // empty instances
113+ MAPPER .configOverride (String .class ).setSetterInfo (JsonSetter .Value .forValueNulls (Nulls .SET ));
114+ MAPPER .configOverride (Integer .class ).setSetterInfo (JsonSetter .Value .forValueNulls (Nulls .SET ));
115+ MAPPER .configOverride (Double .class ).setSetterInfo (JsonSetter .Value .forValueNulls (Nulls .SET ));
116+ MAPPER .configOverride (Boolean .class ).setSetterInfo (JsonSetter .Value .forValueNulls (Nulls .SET ));
117+ }
118+
63119 private DeclarativeConfiguration () {}
64120
65121 /**
@@ -140,8 +196,7 @@ public static OpenTelemetryConfigurationModel parse(InputStream configuration) {
140196 static OpenTelemetryConfigurationModel parse (
141197 InputStream configuration , Map <String , String > environmentVariables ) {
142198 Object yamlObj = loadYaml (configuration , environmentVariables );
143- return YamlObjectMapper .getInstance ()
144- .convertValue (yamlObj , OpenTelemetryConfigurationModel .class );
199+ return MAPPER .convertValue (yamlObj , OpenTelemetryConfigurationModel .class );
145200 }
146201
147202 // Visible for testing
@@ -175,8 +230,7 @@ public static DeclarativeConfigProperties toConfigProperties(InputStream configu
175230 static DeclarativeConfigProperties toConfigProperties (
176231 Object model , ComponentLoader componentLoader ) {
177232 Map <String , Object > configurationMap =
178- YamlObjectMapper .getInstance ()
179- .convertValue (model , new TypeReference <Map <String , Object >>() {});
233+ MAPPER .convertValue (model , new TypeReference <Map <String , Object >>() {});
180234 if (configurationMap == null ) {
181235 configurationMap = Collections .emptyMap ();
182236 }
@@ -197,10 +251,8 @@ public static Sampler createSampler(DeclarativeConfigProperties genericSamplerMo
197251 YamlDeclarativeConfigProperties yamlDeclarativeConfigProperties =
198252 requireYamlDeclarativeConfigProperties (genericSamplerModel );
199253 SamplerModel samplerModel =
200- YamlObjectMapper .getInstance ()
201- .convertValue (
202- DeclarativeConfigProperties .toMap (yamlDeclarativeConfigProperties ),
203- SamplerModel .class );
254+ MAPPER .convertValue (
255+ DeclarativeConfigProperties .toMap (yamlDeclarativeConfigProperties ), SamplerModel .class );
204256 return createAndMaybeCleanup (
205257 SamplerFactory .getInstance (),
206258 DeclarativeConfigContext .create (yamlDeclarativeConfigProperties .getComponentLoader ()),
0 commit comments