YAML #
YAML (YAML Ain’t Markup Language) is a data serialization format designed to be easy for humans to read. Compared to JSON, which uses curly braces and quotes, YAML uses indentation and colons — cleaner for long configuration files. You’ve probably seen YAML many times without realizing it: application.yml in Spring Boot, docker-compose.yml, kubernetes.yaml, CI/CD pipelines. In Java, the main library for working with YAML is SnakeYAML — which Spring Boot also uses behind the scenes. This article covers YAML syntax from the basics, reading and writing YAML with SnakeYAML, full Spring Boot integration, multi-document handling, anchors and aliases to avoid duplication, and configuration validation with Bean Validation.
Basic YAML Syntax #
Before getting into Java code, you need to understand how YAML works. YAML has one main rule: indentation defines hierarchy, and indentation must be consistent (spaces, not tabs).
Data Types and Basic Structures #
# Comments start with #
# Scalars (single values)
name: Budi Santoso
age: 30
height: 175.5
active: true
none: null # or ~
# Strings can be unquoted (in most cases)
city: Jakarta
# Strings with special characters must be quoted
message: "Hello: world" # a colon needs quoting
path: 'C:\Users\Budi' # backslashes are safe with single quotes
# Multi-line with | (literal — newlines preserved)
description: |
First line.
Second line.
Third line.
# Multi-line with > (folded — newlines become spaces)
summary: >
This is a long summary
that will be joined into
one paragraph with spaces.
# Mapping (dictionary / object)
address:
street: Jl. Merdeka No. 1
city: Jakarta
zip_code: 10110
# Sequence (list / array)
hobbies:
- reading
- coding
- cycling
# Inline sequence
colors: [red, green, blue]
# Inline mapping
coordinates: {lat: -6.2, lng: 106.8}
# Nested — mappings inside a sequence
products:
- id: 1
name: Laptop
price: 12000000
- id: 2
name: Mouse
price: 150000
Automatic Data Types #
YAML detects data types automatically — this can be a trap if you’re not careful:
# Numbers
port: 8080 # Integer
ratio: 3.14 # Float
big: 1_000_000 # Integer with separator (1000000)
hex: 0xFF # Hexadecimal (255)
octal: 0o17 # Octal (15)
# Booleans — YAML 1.1 treats all of these as booleans
yes_bool: true # true
no_bool: false # false
# CAREFUL: in YAML 1.1, 'yes', 'no', 'on', 'off' are also booleans!
# To avoid ambiguity, always quote strings that could be misinterpreted
yes_string: "yes" # the string "yes", not a boolean
# Null
empty: null
also_empty: ~
not_there: # an empty value is also null
# Dates (SnakeYAML automatically parses these as java.util.Date)
date: 2025-08-17 # parsed as Date
time: 2025-08-17T09:00:00Z # parsed as Date with time
Anchors and Aliases — Avoiding Duplication #
One YAML feature that doesn’t exist in JSON is the ability to define a value once and reference it elsewhere.
# Define an anchor with &name
database_default: &db_default
host: localhost
port: 5432
username: admin
pool_size: 10
# Use an alias with *name — copies all fields from the anchor
development:
database:
<<: *db_default # merge all fields from db_default
name: dev_db # override a specific field
production:
database:
<<: *db_default
host: db.production.com # override host
name: prod_db
pool_size: 50 # override pool_size
# production.database after the merge:
# host: db.production.com
# port: 5432
# username: admin
# pool_size: 50
# name: prod_db
Multi-Documents #
One YAML file can contain several documents separated by ---:
# Document 1
---
name: Budi
role: admin
# Document 2
---
name: Ani
role: user
# Document 3
---
name: Citra
role: moderator
SnakeYAML #
SnakeYAML is the Java library for reading and writing YAML. It’s the most widely used YAML parser in the Java ecosystem and is a transitive dependency of Spring Boot.
Dependencies #
<!-- Maven -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
// Gradle
implementation 'org.yaml:snakeyaml:2.2'
In Spring Boot projects, SnakeYAML is already available automatically via spring-boot-starter — no need to add dependencies.
Reading YAML #
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;
import java.util.List;
Yaml yaml = new Yaml();
// Read from a string
String content = """
name: Budi
age: 30
active: true
hobbies:
- reading
- coding
""";
Map<String, Object> data = yaml.load(content);
String name = (String) data.get("name"); // "Budi"
int age = (Integer) data.get("age"); // 30
boolean active = (Boolean) data.get("active"); // true
List<String> hobbies = (List<String>) data.get("hobbies"); // ["reading", "coding"]
// Read from a file
try (InputStream is = new FileInputStream("config.yaml")) {
Map<String, Object> config = yaml.load(is);
System.out.println(config);
}
// Read from the classpath (for resources inside a JAR)
try (InputStream is = getClass().getResourceAsStream("/config.yaml")) {
Map<String, Object> config = yaml.load(is);
}
Reading into a Java Object (POJO) #
SnakeYAML can fill a POJO directly — the YAML field names must match the Java field names (or getters/setters).
// The POJO to be filled
public class DatabaseConfig {
private String host;
private int port;
private String name;
private String username;
private String password;
private int poolSize;
// Getters and setters are required for SnakeYAML
public String getHost() { return host; }
public void setHost(String host) { this.host = host; }
// ... other setters and getters
}
// YAML
String yamlConfig = """
host: localhost
port: 5432
name: mydb
username: admin
password: secret123
poolSize: 10
""";
// Load directly into a specific class
Yaml yaml = new Yaml(new Constructor(DatabaseConfig.class, new LoaderOptions()));
DatabaseConfig config = yaml.load(yamlConfig);
System.out.println(config.getHost()); // localhost
System.out.println(config.getPort()); // 5432
System.out.println(config.getPoolSize()); // 10
Reading Multi-Documents #
String multiDoc = """
---
name: Budi
role: admin
---
name: Ani
role: user
---
name: Citra
role: moderator
""";
Yaml yaml = new Yaml();
// loadAll() returns an Iterable over all documents
for (Object doc : yaml.loadAll(multiDoc)) {
Map<String, Object> user = (Map<String, Object>) doc;
System.out.println(user.get("name") + ": " + user.get("role"));
}
// Budi: admin
// Ani: user
// Citra: moderator
Writing YAML #
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.DumperOptions;
// Output configuration
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); // block style (not inline)
options.setIndent(2); // 2-space indentation
options.setPrettyFlow(true);
Yaml yaml = new Yaml(options);
// Write a Map to YAML
Map<String, Object> data = new LinkedHashMap<>(); // LinkedHashMap so order is preserved
data.put("name", "Laptop");
data.put("price", 12_000_000);
data.put("available", true);
data.put("specifications", Map.of("ram", "16GB", "storage", "512GB SSD"));
data.put("tags", List.of("electronics", "computer"));
String yamlOutput = yaml.dump(data);
System.out.println(yamlOutput);
/*
name: Laptop
price: 12000000
available: true
specifications:
ram: 16GB
storage: 512GB SSD
tags:
- electronics
- computer
*/
// Write to a file
try (FileWriter writer = new FileWriter("output.yaml")) {
yaml.dump(data, writer);
}
// Write a POJO directly
DatabaseConfig config = new DatabaseConfig();
config.setHost("localhost");
config.setPort(5432);
String pojoYaml = yaml.dump(config);
// !!com.example.DatabaseConfig
// host: localhost
// port: 5432
// ...
LoaderOptions and DumperOptions Configuration #
import org.yaml.snakeyaml.LoaderOptions;
// LoaderOptions — set security limits
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setMaxAliasesForCollections(50); // alias limit in collections
loaderOptions.setAllowDuplicateKeys(false); // reject duplicate keys
loaderOptions.setCodePointLimit(10 * 1024 * 1024); // file size limit (10MB)
// Important for security: restrict which types can be instantiated
loaderOptions.setTagInspector(tag -> {
// Only allow types from our own package
return tag.startsWith("tag:yaml.org,2002:") ||
tag.contains("com.example.");
});
Yaml yaml = new Yaml(new Constructor(new LoaderOptions()));
SnakeYAML before version 2.0 is vulnerable to YAML deserialization attacks — an attacker can inject dangerous Java types into YAML and cause arbitrary code execution. Always useLoaderOptionswithSafeConstructoror restrict the allowed types when processing YAML from untrusted sources.
YAML with Jackson #
Jackson (already covered in the JSON article) can also read and write YAML with an additional module. This is useful if you’re already using Jackson for JSON and want a consistent API for YAML.
Dependencies #
<!-- Maven -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.17.1</version>
</dependency>
Reading and Writing YAML with Jackson #
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
// Create a YAML-specific ObjectMapper
ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
// POJO
record AppConfig(String appName, int port, DatabaseConfig database) {}
record DatabaseConfig(String host, int port, String name) {}
String yaml = """
appName: My App
port: 8080
database:
host: localhost
port: 5432
name: mydb
""";
// YAML → POJO deserialization (exactly like JSON)
AppConfig config = yamlMapper.readValue(yaml, AppConfig.class);
System.out.println(config.appName()); // My App
System.out.println(config.database().host()); // localhost
// POJO → YAML serialization
AppConfig updated = new AppConfig("New App", 9090,
new DatabaseConfig("db.server.com", 5432, "prod_db"));
// Output configuration
ObjectMapper pretty = new ObjectMapper(
new YAMLFactory()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) // remove "---"
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) // reduce quotes
);
String yamlOutput = pretty.writerWithDefaultPrettyPrinter().writeValueAsString(updated);
System.out.println(yamlOutput);
// Jackson annotations work for YAML too!
public class Product {
@JsonProperty("product_name") // YAML field: "product_name"
private String name;
@JsonIgnore
private String internal;
}
The advantage of using Jackson for YAML over SnakeYAML directly: all Jackson annotations (@JsonProperty, @JsonIgnore, @JsonInclude, etc.) work transparently, and you can share serialization logic between JSON and YAML.
Spring Boot — application.yml #
Spring Boot supports application.yml as an alternative to application.properties. YAML is better suited for nested configuration because the hierarchy is clearer without repeated prefixes.
Comparing application.properties vs application.yml #
# application.properties — repeated prefixes
server.port=8080
server.servlet.context-path=/api
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.datasource.password=secret
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=2
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
# application.yml — clean hierarchy, no repetition
server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: admin
password: secret
hikari:
maximum-pool-size: 10
minimum-idle: 2
jpa:
hibernate:
ddl-auto: validate
show-sql: false
Profiles in YAML #
One advantage of application.yml is defining all profiles in a single file using the --- separator and spring.config.activate.on-profile:
# Default configuration (applies to all profiles unless overridden)
app:
name: Java Application
version: 1.0.0
server:
port: 8080
logging:
level:
root: INFO
---
# development profile
spring:
config:
activate:
on-profile: development
server:
port: 8081
spring:
datasource:
url: jdbc:h2:mem:devdb
username: sa
password:
h2:
console:
enabled: true
logging:
level:
com.example: DEBUG
---
# production profile
spring:
config:
activate:
on-profile: production
server:
port: 80
spring:
datasource:
url: jdbc:postgresql://db.server.com:5432/proddb
username: ${DB_USERNAME} # value from an environment variable
password: ${DB_PASSWORD}
logging:
level:
root: WARN
# Activate the profile when running
java -jar app.jar --spring.profiles.active=production
# or via an environment variable
SPRING_PROFILES_ACTIVE=production java -jar app.jar
Custom Configuration with @ConfigurationProperties #
The best way to read YAML configuration in Spring Boot is with @ConfigurationProperties — it automatically binds the YAML hierarchy to a POJO.
# application.yml
app:
name: Online Store
version: 2.5.0
features:
registration: true
digital-payment: true
email-notification: false
limits:
upload-mb: 10
requests-per-minute: 100
contact:
email: [email protected]
phone: "+62-21-12345678"
database:
host: localhost
port: 5432
name: storedb
pool:
min: 2
max: 20
timeout-seconds: 30
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import jakarta.validation.constraints.*;
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
@NotBlank
private String name;
@NotBlank
private String version;
private Features features = new Features();
private Limits limits = new Limits();
private Contact contact = new Contact();
private Database database = new Database();
// Nested classes
public static class Features {
private boolean registration = true;
private boolean digitalPayment = true; // YAML: digital-payment
private boolean emailNotification = false;
// getters and setters
}
public static class Limits {
@Min(1) @Max(100)
private int uploadMb = 5;
@Min(1)
private int requestsPerMinute = 60;
// getters and setters
}
public static class Contact {
@Email
private String email;
@Pattern(regexp = "\\+?[0-9-]+")
private String phone;
// getters and setters
}
public static class Database {
@NotBlank
private String host;
@Min(1) @Max(65535)
private int port = 5432;
@NotBlank
private String name;
private Pool pool = new Pool();
public static class Pool {
@Min(1)
private int min = 2;
@Min(1)
private int max = 10;
@Min(1)
private int timeoutSeconds = 30;
// getters and setters
}
// getters and setters
}
// Getters and setters for all fields...
}
// Enable @ConfigurationProperties validation
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(AppProperties.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// Use it in other components via dependency injection
@Service
public class InfoService {
private final AppProperties props;
public InfoService(AppProperties props) {
this.props = props;
}
public String getInfo() {
return props.getName() + " v" + props.getVersion();
}
}
Environment Variables and Placeholders #
# Reference environment variables with ${VAR_NAME}
spring:
datasource:
password: ${DB_PASSWORD} # required
username: ${DB_USER:admin} # default "admin" if missing
# Reference other properties
app:
name: My Store
url-base: https://store.com
url-api: ${app.url-base}/api # "https://store.com/api"
url-docs: ${app.url-base}/docs # "https://store.com/docs"
# Values from files (Spring Boot)
spring:
datasource:
password: ${file:/run/secrets/db_password} # read from a file
Configuration Validation #
@ConfigurationProperties fully supports Bean Validation (jakarta.validation). If the configuration is invalid at startup, the application fails immediately with a clear message — far better than errors appearing mid-execution.
Adding Validation #
<!-- Maven — add if not present (Spring Web already includes it) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.*;
@ConfigurationProperties(prefix = "app")
@Validated // enable Bean Validation
public class AppProperties {
@NotBlank(message = "Application name must not be empty")
private String name;
@Min(value = 1, message = "Port must be greater than 0")
@Max(value = 65535, message = "Port must not exceed 65535")
private int port;
@NotNull(message = "Email configuration is required")
@Valid // also validate nested objects
private EmailConfig email;
public static class EmailConfig {
@NotBlank
@Email(message = "Invalid email format")
private String sender;
@NotBlank
private String host;
@Min(1)
private int port = 587;
// getters and setters
}
// getters and setters
}
If any value is invalid at startup:
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException:
Failed to bind properties under 'app' to AppProperties
Reason: app.port: Port must be greater than 0
app.email.sender: Invalid email format
YAML vs JSON vs Properties — When to Use Which #
| Aspect | YAML | JSON | Properties |
|---|---|---|---|
| Readability | ✓ Excellent for hierarchies | Moderate | Poor for deep hierarchies |
| Comments | ✓ Supported | ✗ Not supported | ✓ Supported |
| Data types | ✓ Automatic | ✓ Explicit | ✗ All strings |
| Multi-documents | ✓ Supported | ✗ Not supported | ✗ Not supported |
| Anchors/aliases | ✓ Avoid duplication | ✗ Not supported | ✗ Not supported |
| Parsing | Slower | Faster | Very fast |
| Security | Needs care | Safe | Safe |
| Main use | Config files, Kubernetes, CI/CD | REST APIs, data interchange | Simple Java configuration |
When to Use YAML #
Use YAML when:
✓ Configuration files with lots of hierarchy (Spring Boot, Kubernetes, Docker Compose)
✓ You need comments in configuration files
✓ You want different profiles in one file (with ---)
✓ Repeated configuration that can be simplified with anchors/aliases
✓ The team is more comfortable reading hierarchies than flat properties
Use JSON when:
✗ Data format for REST APIs or inter-service communication
✗ Configuration that will be read by JavaScript / browsers
✗ You don't need comments and automatic types
Use Properties when:
✗ Simple configuration without deep hierarchy
✗ Environments that only support the old format
Be careful with YAML:
✗ Don't process YAML from untrusted sources without SafeConstructor
✗ Tabs aren't allowed for indentation — always use spaces
✗ Inconsistent indentation causes confusing parsing errors
✗ "yes", "no", "on", "off" are interpreted as booleans in YAML 1.1
Summary #
- YAML uses indentation for hierarchy — use spaces (not tabs), and make sure the number of spaces is consistent. Wrong indentation is the most common cause of YAML errors.
- SnakeYAML is the main YAML parsing library in Java. Spring Boot uses it behind the scenes. For direct use, always configure
LoaderOptionswithSafeConstructorwhen processing YAML from external sources.- Jackson + YAMLFactory provides a consistent API between JSON and YAML — all Jackson annotations (
@JsonProperty,@JsonIgnore, etc.) work for YAML too.application.ymlis cleaner thanapplication.propertiesfor nested configuration because there’s no repeated prefix. Use---to define multiple profiles in one file.@ConfigurationPropertiesis the best way to read YAML in Spring Boot — it automatically binds the YAML hierarchy to a POJO, supports nested types, lists, and maps, and can be validated with Bean Validation.- Environment variables via
${VAR_NAME}— separate sensitive configuration (passwords, API keys) into environment variables. Use${VAR_NAME:default_value}for fallback values.- Anchors (
&name) and aliases (*name) prevent configuration duplication — define once, reference many times.<<:merges an entire mapping with selective field overrides.- “yes”, “no”, “on”, “off” are booleans in YAML 1.1 — use quotes if you want these values treated as strings. This is a very common YAML trap.