Skip to content

DeclarativeConfigProperties: no way to coerce types without triggering warningsΒ #8101

Closed

Description

@trask

From open-telemetry/opentelemetry-java-instrumentation#15775 (comment)

Problem

YamlDeclarativeConfigProperties logs WARNING-level messages when a getter is called for a type that doesn't match the stored value:

WARNING: Ignoring value for key [foo] because it is Long instead of String

This means we can't build a type-coercing DeclarativeConfigProperties decorator without producing invalid warnings.

Use case

The Spring Boot starter populates OpenTelemetry's declarative config YAML from Spring's property system. Spring flattens its own YAML into key-value pairs and resolves ${} placeholders via Environment.getProperty(), which always returns String. These flat string properties are then converted back into a nested map and fed to Jackson's ObjectMapper.convertValue() to produce an OpenTelemetryConfigurationModel, which ultimately backs a YamlDeclarativeConfigProperties.

The problem is that the resulting types are inconsistent:

  • Values from plain YAML (e.g. int_key: 42) are stored as their native types (Integer, Boolean, Double)
  • Values resolved through Spring placeholders (e.g. int_key: ${INT_ENV:42}) arrive as String ("42")

We'd like to use a type-coercing decorator around DeclarativeConfigProperties to handle both cases transparently, but there is no way to check the actual stored type without triggering the WARNING-level logs, e.g.

  • Calling delegate.getString("int_key") on a natively-typed Integer logs: "Ignoring value for key [int_key] because it is Integer instead of String"

Activity

  1. jack-berg commented on Feb 18, 2026

    @jack-berg
    Member

    Values resolved through Spring placeholders (e.g. int_key: ${INT_ENV:42}) arrive as String ("42")

    I don't understand this. This should resolve as a tag:yaml.org,2002:int:

    Are you talking about the spring function "Spring flattens its own YAML into key-value pairs and resolves ${} placeholders via Environment.getProperty(), which always returns String."?

    We'd like to use a type-coercing decorator

    What's a type-coercing decorator?

    but there is no way to check the actual stored type without triggering the WARNING-level logs, e.g.

    I'm supported of adding more introspection APIs. Currently we have Set<String> getPropertyKeys(). Some options to improve:

    • Class<?> getType(String key): return the type of a given key
    • boolean is<Type>(String key) (e.g. boolean isString(String key)): boolean checks for each type
  2. trask commented on Feb 18, 2026

    @trask
    MemberAuthor

    Values resolved through Spring placeholders (e.g. int_key: ${INT_ENV:42}) arrive as String ("42")

    I don't understand this. This should resolve as a tag:yaml.org,2002:int

    sorry for confusion, above statement is about Spring YAML (not OpenTelemetry YAML)

    What's a type-coercing decorator?

    e.g. we want to implement ConfigProvider so that calls to DeclarativeConfigProperties.getInt() automatically coerce Strings to Ints where possible

  3. Tusharika725 commented on Apr 2, 2026

    @Tusharika725
    Contributor

    @trask @jack-berg I would love to pick this up as my next issue!

    To allow for safe type-coercion without triggering the WARNING logs, adding introspection APIs makes perfect sense. Between Jack's two proposals:

    Class<?> getType(String key)
    boolean is(String key) (e.g., isString(), isInteger())

    Is there a preference from the maintainer team on which approach aligns better with the current DeclarativeConfigProperties design? Once there is consensus on the preferred method signature, I'm happy to open a PR adding it to the interface and implementing it across the backing classes!

  4. colamong commented on Sep 21, 2026

    @colamong
    Contributor

    @Tusharika725 are you still on this? Happy to take it over if not.

    I hit the downstream symptom in the Spring starter: debug: true resolved through Spring arrives as a String, so getBoolean returned false and span logging could never be enabled (open-telemetry/opentelemetry-java-instrumentation#20034). The workaround there is a copied DeclarativeConfigProperties that coerces β€” the copy exists only because there is no way to ask the delegate what it actually holds.

    Of Jack's two options I'd take boolean is<Type>(String key). It maps 1:1 onto the existing getters, so a decorator just asks "would getX succeed" before calling it, and it keeps the interface from committing to concrete classes (Integer vs Long vs Double). getType is one method instead of several, but callers then switch on types the interface doesn't otherwise expose.

    I'll open a PR with that unless someone prefers getType.

  5. jack-berg commented on Sep 24, 2026

    @jack-berg
    Member

    I'm supported of adding more introspection APIs. Currently we have Set getPropertyKeys(). Some options to improve:

    Class<?> getType(String key): return the type of a given key
    boolean is(String key) (e.g. boolean isString(String key)): boolean checks for each type

    @trask Would the addition of one or both of these allow you to write the type-coercing decorator you need for spring?

  6. colamong commented on Sep 28, 2026

    @colamong
    Contributor

    @jack-berg concrete answer, since it landed downstream three days ago: yes, either option unblocks it.

    open-telemetry/opentelemetry-java-instrumentation#20034 shipped SpringDeclarativeConfigProperties β€” not a decorator but a full copy of YamlDeclarativeConfigProperties with coercing getters, carrying a TODO: Revisit after #8101. That copy gets deleted when this lands.

    I'd take is<Type>. In that copy getInt has to accept both Integer and Long; with getType() every caller re-derives that widening rule, whereas is<Type> just asks "would getX succeed". Cost is five methods against one.

    @Tusharika725 I asked on 21 Sep if you were still on this β€” no reply, so unless you object I'll take it.

    Unless there's an objection to the signature, I'll open a PR adding is<Type> to the interface plus EmptyDeclarativeConfigProperties and YamlDeclarativeConfigProperties.

  7. jack-berg commented on Sep 29, 2026

    @jack-berg
    Member

    I'd take is. In that copy getInt has to accept both Integer and Long; with getType() every caller re-derives that widening rule, whereas is just asks "would getX succeed". Cost is five methods against one.

    Unless there's an objection to the signature, I'll open a PR adding is to the interface plus EmptyDeclarativeConfigProperties and YamlDeclarativeConfigProperties.

    Let's do it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions