Spring Boot YAML to List of Objects: Collections & Nested Types Explained
Master binding YAML lists to Java objects in Spring Boot 3.x.
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
- ✓Java 17+ and Spring Boot 3.x (3.2+ recommended for best YAML support)
- ✓Basic understanding of @ConfigurationProperties and application.yml structure
- ✓Familiarity with Maven/Gradle dependency management for spring-boot-configuration-processor
โข Use @ConfigurationProperties with a class annotated with @ConfigurationProperties(prefix="...") and a List
Think of a YAML list like a shopping list with items that have details. A simple list is like "eggs, milk, bread" โ just strings. A list of objects is like a recipe list where each item has a name, quantity, and unit: "egg, 2, pieces" and "milk, 1, liter". In Spring Boot, you write the YAML structure with dashes and indentation, and Java automatically creates objects from it, just like a robot reading your recipe card and filling out a form for each ingredient.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Here's a story from last year: I was debugging a payment-processing microservice that suddenly started ignoring half the merchant configurations. We had a YAML list of 'acceptedPaymentMethods' โ each with nested fields like 'methodName', 'feePercentage', and 'supportedCurrencies'. The YAML was perfectly valid, but Spring Boot was only loading the first two items. The rest were silently dropped. No errors, no warnings. Just lost revenue. That's the kind of problem you get when you assume YAML-to-object binding is trivial. It's not. Spring Boot's @ConfigurationProperties is powerful, but it has sharp edges โ especially with lists, nested types, and immutable classes. In this tutorial, I'll show you exactly how to bind YAML lists to Java objects correctly, avoid the silent failures I've seen in production (including that payment processor), and handle edge cases like empty lists, null values, and deeply nested structures. You'll learn the official approach with Spring Boot 3.2+, but also the hard-won lessons from real incidents. By the end, you'll be able to model complex configuration hierarchies without fear, validate them at startup, and write tests that catch the subtle bugs that slip through code review.
Binding YAML Lists to Java Objects: The Basics
Spring Boot's @ConfigurationProperties is the idiomatic way to bind structured YAML to POJOs. For lists, the YAML list syntax (- item) maps directly to List<String>, List<Integer>, or lists of custom objects. Here's the hard truth: most teams get this wrong by using @Value with SpEL, which is brittle and doesn't support nested types. Instead, create a dedicated configuration class annotated with @ConfigurationProperties(prefix = "myapp"). For example, given a YAML like:
``yaml myapp: servers: - url: https://api.example.com timeout: 5000 - url: https://backup.example.com timeout: 10000 ``
You can bind it to:
``java @ConfigurationProperties(prefix = "myapp") public class AppProperties { private List<ServerConfig> servers = new ArrayList<>(); // getters and setters } ``
Where ServerConfig is a simple POJO with url and timeout. Enable this with @EnableConfigurationProperties(AppProperties.class) on a @Configuration class. The binding happens eagerly at startup; any mismatch in types (e.g., string where number expected) throws a ConversionFailedException. Always initialize collections to empty lists to avoid null pointers in production.
- host1
- host2
, you can useList<String> hosts`. Spring Boot uses SnakeYAML internally (version 2.0+ in Spring Boot 3.x), which respects YAML spec 1.2. Remember: indentation matters โ two spaces per level, no tabs.
What the Official Docs Won't Tell You
The official Spring Boot documentation glosses over several landmines. First: YAML lists of objects require mutable POJOs with public setters โ no immutable record types unless you use constructor binding with @ConstructorBinding (Spring Boot 2.2+). If you use a record and forget @ConstructorBinding, you get a confusing BindingException at startup. Second: YAML merging with anchors (& and <<:) works in SnakeYAML, but Spring Boot's ConfigurationProperties binder does NOT support YAML anchors for list elements. I've seen teams try to DRY up configs with anchors only to find the binder silently skips the merged elements. Third: nested lists within lists (e.g., a list of objects each containing a list) require careful flattening โ the binder expects a nested YAML structure, not a flat one. For example:
``yaml myapp: clusters: - name: prod nodes: - host: node1 - host: node2 ``
Binds to List<ClusterConfig> where ClusterConfig has List<NodeConfig> nodes. But if you write:
``yaml myapp: clusters: - name: prod - nodes: - host: node1 ``
That becomes two separate objects in the list โ a bug that's hard to catch without integration tests. Finally: Spring Boot 3.x with SnakeYAML 2.0 changed how it handles duplicate keys โ it now throws an exception by default instead of taking the last value. This broke many configs that had accidental duplicates.
Binding Maps and Collections with Mixed Types
YAML supports maps (`key: value) and collections of maps. Spring Boot binds these to Map<String, Object> or Map<String, List<Something>>`. A common pattern is a list of maps โ e.g., a list of feature flags with metadata:
``yaml myapp: features: - name: dark-mode enabled: true rollout: 50 - name: new-checkout enabled: false rollout: 0 ``
This binds to List<Map<String, Object>> if you don't define a POJO, but that's a code smell โ you lose type safety. Instead, always define a POJO like FeatureFlag. For truly dynamic configs where keys are unknown at compile time, use Map<String, List<String>>. For example:
``yaml myapp: endpoint-groups: us-east: - https://east1.example.com - https://east2.example.com eu-west: - https://west1.example.com ``
Binds to Map<String, List<String>> endpointGroups. The key insight: YAML keys become map keys, and the list values become List<String>. Spring Boot handles type conversion automatically for primitives, strings, and enums. For custom types, you need a Converter bean. One gotcha: YAML treats null values as absent โ if a key has a null value (e.g., key:), the map entry is excluded entirely. This can silently drop expected config entries. Always use @DefaultValue or set defaults in the POJO constructor for robustness.
For mixed collections (e.g., a list containing both strings and maps), YAML supports it but Java's type system doesn't without Object. Avoid this โ it's a maintenance nightmare. Instead, use a wrapper POJO with a String type discriminator.
Validation and Defaults for YAML-Listed Configurations
Production-grade applications must validate configuration at startup, not at first use. Spring Boot supports @Validated on @ConfigurationProperties classes with JSR-380 Bean Validation (e.g., @NotBlank, @Min, @NotEmpty). For lists, you can validate each element by annotating the list field with @Valid:
``java @Valid private List<@Valid ServerConfig> servers; ``
This triggers validation on each ServerConfig object. The @NotEmpty annotation on the list itself ensures at least one element exists. For defaults, initialize fields directly in the POJO โ this is safer than relying on YAML defaults because Spring Boot merges defaults from multiple sources (e.g., application.yml, profile-specific files, environment variables). Environment variables override YAML lists in a non-intuitive way: a list in YAML can be replaced entirely by a comma-separated environment variable (e.g., MYAPP_SERVERS[0]_URL). This is fine for simple lists but breaks for nested objects โ you'd need indexed properties in the env var name, which is error-prone.
Another pro tip: use @DurationUnit and @DataSizeUnit for timeouts and sizes in lists to avoid manual parsing. For example:
``yaml myapp: timeouts: - endpoint: /api duration: 5s ``
Binds to List<TimeoutConfig> with @DurationUnit(ChronoUnit.SECONDS) private Duration duration. This saves you from writing custom converters.
Finally, always add a @PostConstruct validation method in the configuration class to cross-validate list elements (e.g., no duplicate URLs, timeout ranges). This catches misconfigurations before the application accepts traffic.
Advanced YAML Binding: Nested Maps and Collections of Complex Objects
Once you've mastered flat lists, the real power of YAML in Spring Boot emerges when dealing with nested structures. Let's say you're building a payment-processing system that needs to configure multiple gateways, each with its own set of endpoints and credentials. Here's the hard truth: most teams get this wrong by trying to force everything into a single flat properties file. Instead, use YAML's natural hierarchy. Define a top-level key like payment.gateways that maps to a list of objects. Each object can have nested maps for endpoints (e.g., sandbox, production) and even nested lists for retry strategies. For example, a Stripe gateway might have endpoints: {sandbox: "https://sandbox.stripe.com", production: "https://api.stripe.com"} and retry: [100, 200, 500] (milliseconds). Spring Boot's @ConfigurationProperties with a List<GatewayConfig> will bind this flawlessly, as long as your class has a List<GatewayConfig> field with getters/setters. The nested map becomes a Map<String, String> in Java, and the nested list becomes a List<Integer>. Use @Valid and @NotEmpty on fields to enforce constraints at startup. I've seen production outages because a missing endpoint caused a NullPointerException during runtime โ validate early. The @ConstructorBinding (available since Spring Boot 2.2) is your friend for immutable objects; use it with @DefaultValue for optional fields. Remember: YAML's indentation is sacred โ one wrong space and your entire binding fails silently.
org.springframework.boot.context.properties. Always add logging.level.org.springframework.boot.context.properties=DEBUG in dev to see binding details.@ConstructorBinding with records for immutable configuration objects. Validate nested structures with @Valid and @NotEmpty to catch misconfigurations at startup, not at 3 AM.Binding YAML Lists to Polymorphic Types Using @JsonTypeInfo
Sometimes you need a list of objects that share a common interface but have different implementations. For example, in a SaaS billing system, you might have a list of DiscountStrategy objects: PercentageDiscount and FixedAmountDiscount. YAML can handle this using a discriminator field. Spring Boot's @ConfigurationProperties doesn't natively support polymorphism, but you can leverage Jackson's @JsonTypeInfo and @JsonSubTypes annotations on your interface. Here's the trick: Spring Boot uses Jackson internally for YAML binding (via SnakeYAML), so these annotations work seamlessly. Define an abstract class DiscountStrategy with @JsonTypeInfo(use = Id.NAME, property = "type") and @JsonSubTypes({@Type(value = PercentageDiscount.class, name = "percentage"), @Type(value = FixedAmountDiscount.class, name = "fixed")}). Then in your YAML, each object in the list must have a type field: - type: percentage, rate: 0.1. Spring Boot will instantiate the correct subclass. I've used this in production for feature flags with different implementation strategies. The gotcha: the property name in @JsonTypeInfo must match exactly the key in your YAML. Misspelling type as typo will silently create a generic LinkedHashMap instead of your subclass. Always add a test that loads the configuration and checks the actual runtime type. Since Spring Boot 2.2, you can also use @ConfigurationProperties on a @Bean method if you need more control, but for most cases, the Jackson approach is cleaner. Remember that this only works for @ConfigurationProperties binding, not for @Value injection.
type field in a new discount type, causing a ClassCastException in the pricing engine. We added a @PostConstruct validation method that iterated through all strategies and logged their types. Catch these early.@JsonTypeInfo and @JsonSubTypes. Always include a discriminator field (e.g., type) in your YAML to avoid silent fallback to LinkedHashMap.Default Values and Optional Fields in Nested YAML Lists
Not every field in your YAML list needs to be specified. Spring Boot provides several ways to handle defaults. For primitive types like int and boolean, you can set default values directly in your Java class: private int timeout = 5000;. For String fields, use private String name = "default";. But what about nested objects? If a YAML list entry omits a nested map, you get null. To avoid NPEs, initialize the field: private Map<String, String> metadata = new HashMap<>();. For lists of objects, if you omit the entire list, you get null โ so initialize with List.of() if an empty list is acceptable. Since Spring Boot 2.2, @DefaultValue annotation on constructor parameters provides defaults for immutable objects. For example, @DefaultValue("5000") int timeout. The value must be a string; Spring converts it. One common mistake: assuming that YAML's null or empty value will be overridden by a default. Actually, YAML's ~ (null) or missing key will leave the field as its Java default (e.g., null for objects, 0 for ints). If you want a non-null default, you must set it in Java. Also, beware of YAML's true, false, yes, no parsing for booleans. Spring Boot's relaxed binding will convert yes to true, but enabled: yes works. However, for consistency, always use true/false. In a production monitoring system, I saw a configuration where retryEnabled: on was interpreted as true due to YAML's boolean handling, but on is not a standard Spring Boot boolean value โ it worked by accident. Stick to true/false to avoid confusion. For nested lists of complex objects, if an entry has missing fields, Spring Boot will create the object with null fields unless defaults are specified. Use @Builder with @DefaultValue for a clean approach.
hosts vs host), causing the list to be null. The service silently used an empty list and failed with a cryptic error. We added a @PostConstruct validation that logged all configuration values and threw an exception if critical lists were empty. Saved us hours of debugging.@DefaultValue). YAML's missing keys result in null or primitive default (0, false). Always initialize collections to avoid NPEs.Debugging YAML Binding: Common Failures and How to Fix Them
YAML binding failures are notoriously silent. You might define a list of objects in YAML, but Spring Boot silently ignores it if the structure doesn't match. Here's my go-to debugging checklist. First, enable debug logging: logging.level.org.springframework.boot.context.properties=DEBUG. This will show you exactly what Spring Boot is trying to bind. Second, use the @ConfigurationPropertiesScan annotation (Spring Boot 2.2+) to automatically detect @ConfigurationProperties beans without manual @EnableConfigurationProperties. Third, check your YAML indentation: every nested level must be exactly 2 spaces. A common mistake is mixing spaces and tabs. Fourth, verify that your Java class has appropriate getters and setters (or is a record). Spring Boot uses setter injection by default; if you use @ConstructorBinding, ensure the constructor is public and parameters match the YAML keys. Fifth, for lists, the YAML syntax must be either list: [item1, item2] (inline) or list: - item1 - item2 (block). Mixing both in the same file can cause issues. Sixth, check for key naming: Spring Boot's relaxed binding (e.g., my-prop, myProp, MY_PROP) is forgiving, but for lists, the keys must match exactly. If your YAML has my-list[0].name, the Java field must be myList (a List<Something>). The [0] index is automatically handled. Seventh, if you're using @Validated with @NotEmpty on a list, Spring Boot will throw a BindException at startup if the list is empty. This is a good practice. Finally, use the spring-boot-configuration-processor dependency to generate metadata for your custom properties, which gives you auto-completion in IDEs and helps catch typos. In a recent project, a colleague spent 2 hours debugging a list that never populated โ turns out the YAML file had a BOM (Byte Order Mark) from a Windows editor. Use a linter like yamllint to catch these issues before runtime.
yamllint and checks for BOM characters. Also, use @PostConstruct to log the configuration at startup in production โ this saved us during a deployment where the wrong YAML file was copied.org.springframework.boot.context.properties to see binding details. Use yamllint to validate YAML syntax. Always verify with a unit test that loads the configuration.Silent YAML List Truncation in Payment Processing
- Always validate list sizes and content types in configuration tests, not just at runtime.
- Use @Validated with jakarta.validation constraints on lists to catch truncation early.
- Never trust YAML indentation โ use a linter or IDE formatter for YAML files.
- Enable spring-boot-configuration-processor to get warnings about mismatched types at compile time.
org.springframework.boot.context.properties. 2. Check YAML file for indentation errors using yamllint. 3. Verify the class has getters/setters or uses @ConstructorBinding. 4. Ensure the YAML file is in the classpath and named application.yml or application-{profile}.yml.@JsonTypeInfo annotation is correctly placed on the base class. 3. Add a unit test to verify the runtime type of each list element.@EnableConfigurationProperties or @ConfigurationPropertiesScan is used. 2. Check for BOM characters in the YAML file. 3. Verify the prefix in @ConfigurationProperties matches the YAML structure. 4. Look for typos in the YAML key names.@DefaultValue). 2. Check that the YAML key is actually missing, not set to null or empty. 3. For @ConstructorBinding, ensure @DefaultValue annotation is used on constructor parameters.logging.level.org.springframework.boot.context.properties=DEBUGyamllint application.yml| File | Command / Code | Purpose |
|---|---|---|
| AppProperties.java | @Component | Binding YAML Lists to Java Objects |
| ClusterConfig.java | @Component | What the Official Docs Won't Tell You |
| FeatureFlags.java | @Component | Binding Maps and Collections with Mixed Types |
| ValidatedAppProperties.java | @Component | Validation and Defaults for YAML-Listed Configurations |
| GatewayConfig.java | @Validated | Advanced YAML Binding |
| DiscountConfig.java | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") | Binding YAML Lists to Polymorphic Types Using @JsonTypeInfo |
| ServiceConfig.java | @ConstructorBinding | Default Values and Optional Fields in Nested YAML Lists |
| application.yml | logging: | Debugging YAML Binding |
Key takeaways
@ConstructorBinding with records for immutable and concise configuration objects.org.springframework.boot.context.properties to troubleshoot binding issues.@Validated and @PostConstruct to catch errors early.@JsonTypeInfo for polymorphic YAML list binding with a discriminator field.Interview Questions on This Topic
How does Spring Boot bind a YAML list of objects to a Java configuration class?
@ConfigurationProperties with a prefix. The Java class must have a List<SomeType> field with getters and setters (or be a record with @ConstructorBinding). The YAML structure must match the field names, with list items defined using - under the key. Spring Boot automatically converts the YAML nodes to Java objects using property editors or converters.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Drawn from code that ran under real load.
That's Spring Boot. Mark it forged?
8 min read · try the examples if you haven't