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 use LoaderOptions with SafeConstructor or 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 #

AspectYAMLJSONProperties
Readability✓ Excellent for hierarchiesModeratePoor 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
ParsingSlowerFasterVery fast
SecurityNeeds careSafeSafe
Main useConfig files, Kubernetes, CI/CDREST APIs, data interchangeSimple 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 LoaderOptions with SafeConstructor when 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.yml is cleaner than application.properties for nested configuration because there’s no repeated prefix. Use --- to define multiple profiles in one file.
  • @ConfigurationProperties is 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.

← Previous: JSON   Next: MySQL →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact