or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin-jmx.mdansi-support.mdaot-native-image.mdapplication-info.mdavailability.mdbootstrap.mdbootstrapping.mdbuilder.mdcloud-platform.mdconfiguration-annotations.mdconfiguration-data.mdconfiguration-properties.mdconversion.mddiagnostics.mdenvironment-property-sources.mdindex.mdjson-support.mdlifecycle-events.mdlogging.mdorigin-tracking.mdresource-loading.mdretry-support.mdssl-tls.mdstartup-metrics.mdsupport.mdsystem-utilities.mdtask-execution.mdthreading.mdutilities.mdvalidation.mdweb-support.md
tile.json

conversion.mddocs/

Type Conversion

Package: org.springframework.boot.convert Module: org.springframework.boot:spring-boot Since: 2.0.0

The convert package provides enhanced type conversion capabilities beyond Spring Framework's standard conversion service. It includes specialized converters for durations, periods, data sizes, and delimited strings, along with annotations for controlling conversion behavior.

Quick Reference

ComponentPurposeThread SafetyKey Features
ApplicationConversionServiceEnhanced conversion serviceThread-safe for readsDuration, Period, DataSize, delimited string converters
DurationStyleDuration format parser/printerImmutable, thread-safeSIMPLE ("10s"), ISO8601 ("PT10S")
PeriodStylePeriod format parser/printerImmutable, thread-safeSIMPLE ("1y2m3d"), ISO8601 ("P1Y2M3D")
@DurationFormatControl duration formatN/A (annotation)Specify SIMPLE or ISO8601 style
@DurationUnitDefault duration unitN/A (annotation)Specify ChronoUnit when no suffix
@PeriodFormatControl period formatN/A (annotation)Specify SIMPLE or ISO8601 style
@PeriodUnitDefault period unitN/A (annotation)Specify ChronoUnit when no suffix
@DataSizeUnitDefault data size unitN/A (annotation)Specify DataUnit (B, KB, MB, GB, TB)
@DelimiterCollection delimiterN/A (annotation)Specify delimiter for string splitting

Core Components

ApplicationConversionService

Enhanced conversion service with Spring Boot-specific converters pre-registered.

Thread Safety: Thread-safe for all conversion operations. Immutable after construction. The shared instance is safely published using double-checked locking.

package org.springframework.boot.convert;

import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.util.StringValueResolver;
import org.springframework.core.convert.TypeDescriptor;
import java.util.Map;

/**
 * A specialization of FormattingConversionService configured by default
 * with converters and formatters appropriate for most Spring Boot applications.
 *
 * Designed for direct instantiation but also exposes the static
 * addApplicationConverters and addApplicationFormatters utility methods
 * for ad-hoc use against registry instance.
 *
 * Thread Safety: Thread-safe for all read operations (conversions).
 * Should not be modified after construction in multi-threaded environments.
 *
 * @since 2.0.0
 */
public class ApplicationConversionService extends FormattingConversionService {

    /**
     * Create a new ApplicationConversionService with default converters and formatters.
     * Registers all Spring Boot specific converters including:
     * - Duration converters (String to Duration, Number to Duration)
     * - Period converters (String to Period, Number to Period)
     * - DataSize converters (String to DataSize, Number to DataSize)
     * - Delimited string converters (for collections)
     * - Lenient enum converters (case-insensitive)
     * - File converters
     * - And more...
     */
    public ApplicationConversionService() {
        this(null);
    }

    /**
     * Create a new ApplicationConversionService with an optional embedded
     * value resolver for placeholder resolution.
     *
     * @param embeddedValueResolver the embedded value resolver to use for
     *                              resolving ${...} placeholders and #{...}
     *                              SpEL expressions (may be null)
     */
    public ApplicationConversionService(@Nullable StringValueResolver embeddedValueResolver) {
        // Registers default Spring converters
        // Then adds Spring Boot specific converters via configure()
    }

    /**
     * Add a Printer to be used under the given type.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param printer the printer to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addPrinter(Printer<?> printer) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a Parser to be used under the given type.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param parser the parser to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addParser(Parser<?> parser) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a Formatter to be used under the given type.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param formatter the formatter to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addFormatter(Formatter<?> formatter) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a Formatter to be used for the given field type.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param fieldType the field type to format
     * @param formatter the formatter to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a Printer/Parser pair to be used for the given field type.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param fieldType the field type to format
     * @param printer the printer to add
     * @param parser the parser to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a AnnotationFormatterFactory to be used for fields annotated with a specific annotation.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param annotationFormatterFactory the annotation formatter factory to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addFormatterForFieldAnnotation(
            AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a plain Converter to this registry.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param converter the converter to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addConverter(Converter<?, ?> converter) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a plain Converter for the given source/target type pair.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param sourceType the source type to convert from
     * @param targetType the target type to convert to
     * @param converter the converter to add
     * @param <S> the source type
     * @param <T> the target type
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType,
            Converter<? super S, ? extends T> converter) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a GenericConverter to this registry.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param converter the generic converter to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addConverter(GenericConverter converter) {
        // Checks modifiability before delegating to super
    }

    /**
     * Add a ConverterFactory to this registry.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param factory the converter factory to add
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void addConverterFactory(ConverterFactory<?, ?> factory) {
        // Checks modifiability before delegating to super
    }

    /**
     * Remove any converters from sourceType to targetType.
     * Throws UnsupportedOperationException if this is an unmodifiable shared instance.
     *
     * @param sourceType the source type
     * @param targetType the target type
     * @throws UnsupportedOperationException if the conversion service is unmodifiable
     */
    @Override
    public void removeConvertible(Class<?> sourceType, Class<?> targetType) {
        // Checks modifiability before delegating to super
    }

    /**
     * Return true if objects of sourceType can be converted to the targetType
     * and the converter has Object.class as a supported source type.
     * This is useful for detecting generic ObjectTo* converters.
     *
     * @param sourceType the source type to test (must not be null)
     * @param targetType the target type to test (must not be null)
     * @return true if conversion happens through an ObjectTo... converter
     * @throws IllegalArgumentException if sourceType or targetType is null
     * @since 2.4.3
     */
    public boolean isConvertViaObjectSourceType(
            TypeDescriptor sourceType,
            TypeDescriptor targetType) {
        // Checks if conversion path goes through Object source type
    }

    /**
     * Return a shared default application ConversionService instance,
     * lazily building it once needed. This is the recommended way to
     * obtain a conversion service in most applications.
     *
     * Note: This method actually returns an ApplicationConversionService
     * instance. However, the ConversionService signature has been preserved
     * for binary compatibility.
     *
     * Thread Safety: This method is thread-safe. The instance is created
     * using double-checked locking and safely published.
     *
     * @return the shared ApplicationConversionService instance (never null)
     */
    public static ConversionService getSharedInstance() {
        // Double-checked locking for lazy initialization
        // Returns singleton instance
    }

    /**
     * Configure the given FormatterRegistry with formatters and converters
     * appropriate for most Spring Boot applications.
     *
     * This is the main entry point for adding Spring Boot converters to an
     * existing conversion service.
     *
     * @param registry the registry of converters to add to (must also be
     *                 castable to ConversionService, must not be null)
     * @throws ClassCastException if the given FormatterRegistry could not
     *                            be cast to a ConversionService
     * @throws IllegalArgumentException if registry is null
     */
    public static void configure(FormatterRegistry registry) {
        // Adds default Spring Framework converters
        // Adds Spring Boot application converters
        // Adds Spring Boot application formatters
    }

    /**
     * Add converters useful for most Spring Boot applications.
     * Includes converters for Duration, Period, DataSize, delimited strings,
     * and more.
     *
     * Registered Converters:
     * - StringToDurationConverter (supports @DurationUnit, @DurationFormat)
     * - NumberToDurationConverter
     * - DurationToNumberConverter
     * - DurationToStringConverter
     * - StringToPeriodConverter (supports @PeriodUnit, @PeriodFormat)
     * - NumberToPeriodConverter
     * - PeriodToStringConverter
     * - StringToDataSizeConverter (supports @DataSizeUnit)
     * - NumberToDataSizeConverter
     * - StringToFileConverter
     * - InputStreamSourceToByteArrayConverter
     * - LenientObjectToEnumConverterFactory (case-insensitive)
     * - Delimited string converters (see addDelimitedStringConverters)
     *
     * @param registry the registry of converters to add to (must also be
     *                 castable to ConversionService, must not be null)
     * @throws ClassCastException if the given ConverterRegistry could not
     *                            be cast to a ConversionService
     * @throws IllegalArgumentException if registry is null
     */
    public static void addApplicationConverters(ConverterRegistry registry) {
        // Registers all Spring Boot specific converters
    }

    /**
     * Add converters to support delimited strings.
     * These converters handle splitting strings into collections and arrays,
     * and joining collections/arrays into delimited strings.
     *
     * Registered Converters:
     * - ArrayToDelimitedStringConverter (array to comma-separated string)
     * - CollectionToDelimitedStringConverter (collection to comma-separated string)
     * - DelimitedStringToArrayConverter (comma-separated string to array)
     * - DelimitedStringToCollectionConverter (comma-separated string to collection)
     *
     * These converters respect the @Delimiter annotation when present.
     *
     * @param registry the registry of converters to add to (must also be
     *                 castable to ConversionService, must not be null)
     * @throws ClassCastException if the given ConverterRegistry could not
     *                            be cast to a ConversionService
     * @throws IllegalArgumentException if registry is null
     */
    public static void addDelimitedStringConverters(ConverterRegistry registry) {
        // Registers delimited string converters with @Delimiter support
    }

    /**
     * Add formatters useful for most Spring Boot applications.
     *
     * Registered Formatters:
     * - CharArrayFormatter (char[] to String and vice versa)
     * - InetAddressFormatter (InetAddress to String and vice versa)
     * - IsoOffsetFormatter (OffsetDateTime/OffsetTime formatting)
     *
     * @param registry the service to register default formatters with (must not be null)
     * @throws IllegalArgumentException if registry is null
     */
    public static void addApplicationFormatters(FormatterRegistry registry) {
        // Registers Spring Boot specific formatters
    }

    /**
     * Add Printer, Parser, Formatter, Converter, ConverterFactory,
     * GenericConverter, and beans from the specified bean factory.
     *
     * This method discovers all converter beans in the application context
     * and registers them with the conversion service.
     *
     * @param registry the service to register beans with (must not be null)
     * @param beanFactory the bean factory to get the beans from (must not be null)
     * @throws IllegalArgumentException if registry or beanFactory is null
     * @since 2.2.0
     */
    public static void addBeans(
            FormatterRegistry registry,
            ListableBeanFactory beanFactory) {
        // Discovers and registers converter beans without qualifier filtering
        addBeans(registry, beanFactory, null);
    }

    /**
     * Add Printer, Parser, Formatter, Converter, ConverterFactory,
     * GenericConverter, and beans from the specified bean factory that
     * match the specified qualifier.
     *
     * This method discovers converter beans with specific qualifiers
     * (e.g., @Qualifier("myConverters")) and registers them.
     *
     * @param registry the service to register beans with (must not be null)
     * @param beanFactory the bean factory to get the beans from (must not be null)
     * @param qualifier the qualifier required on the beans or null for no filtering
     * @return a map of bean names to registered bean instances (never null)
     * @throws IllegalArgumentException if registry or beanFactory is null
     * @since 3.5.0
     */
    public static Map<String, Object> addBeans(
            FormatterRegistry registry,
            ListableBeanFactory beanFactory,
            String qualifier) {
        // Discovers and registers qualified converter beans
        // Returns map of registered bean names to instances
    }
}

DurationStyle

Enumeration defining how duration values are parsed and formatted.

Thread Safety: Enums are immutable and thread-safe. All parsing and printing operations are stateless.

package org.springframework.boot.convert;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.regex.Pattern;

/**
 * Duration format styles for parsing and printing Duration values.
 *
 * Thread Safety: Immutable and thread-safe. All operations are stateless.
 *
 * @since 2.0.0
 */
public enum DurationStyle {

    /**
     * Simple formatting, for example '1s', '10ms', '5m'.
     * Supports human-readable duration strings with optional unit suffixes.
     *
     * Pattern: ^([+-]?\\d+)([a-zA-Z]{0,2})$
     *
     * Supported Units:
     * - ns = nanoseconds
     * - us = microseconds
     * - ms = milliseconds (default if no unit specified)
     * - s = seconds
     * - m = minutes
     * - h = hours
     * - d = days
     *
     * Examples:
     * - "10" -> 10 milliseconds (default)
     * - "10s" -> 10 seconds
     * - "5m" -> 5 minutes
     * - "2h" -> 2 hours
     * - "1d" -> 1 day
     * - "-10s" -> negative 10 seconds
     */
    SIMPLE("^([+-]?\\d+)([a-zA-Z]{0,2})$") {

        @Override
        public Duration parse(String value, ChronoUnit unit) {
            // Parses simple format with optional unit suffix
            // Uses provided unit as default if no suffix present
            // Supports positive and negative values
        }

        @Override
        public String print(Duration value, ChronoUnit unit) {
            // Prints in simple format with appropriate unit
            // Chooses most concise representation
            // e.g., 1000ms printed as "1s"
        }
    },

    /**
     * ISO-8601 formatting, for example 'PT20.345S', 'PT15M', 'PT10H'.
     * Uses standard ISO-8601 duration format (PnDTnHnMnS).
     *
     * Pattern: ^[+-]?[pP].*$
     *
     * Format Components:
     * - P = period designator (required)
     * - nD = number of days
     * - T = time designator (required if hour/minute/second specified)
     * - nH = number of hours
     * - nM = number of minutes
     * - nS = number of seconds (can be fractional)
     *
     * Examples:
     * - "PT10S" -> 10 seconds
     * - "PT5M" -> 5 minutes
     * - "PT2H" -> 2 hours
     * - "P2D" -> 2 days
     * - "PT1H30M" -> 1 hour 30 minutes
     * - "PT0.5S" -> 500 milliseconds
     */
    ISO8601("^[+-]?[pP].*$") {

        @Override
        public Duration parse(String value, ChronoUnit unit) {
            // Delegates to Java's Duration.parse()
            // Ignores unit parameter (format is self-contained)
        }

        @Override
        public String print(Duration value, ChronoUnit unit) {
            // Uses Java's Duration.toString()
            // Returns ISO-8601 format
        }
    };

    private final Pattern pattern;

    DurationStyle(String pattern) {
        this.pattern = Pattern.compile(pattern);
    }

    /**
     * Parse the given value to a duration using the default unit (milliseconds).
     *
     * @param value the value to parse (must not be null)
     * @return a duration (never null)
     * @throws IllegalArgumentException if value is null or cannot be parsed
     */
    public Duration parse(String value) {
        return parse(value, null);
    }

    /**
     * Parse the given value to a duration.
     *
     * @param value the value to parse (must not be null)
     * @param unit the duration unit to use if the value doesn't specify one
     *             (null will default to milliseconds for SIMPLE style,
     *              ignored for ISO8601 style)
     * @return a duration (never null)
     * @throws IllegalArgumentException if value is null or cannot be parsed
     * @throws java.time.format.DateTimeParseException if ISO8601 format is invalid
     */
    public abstract Duration parse(String value, ChronoUnit unit);

    /**
     * Print the specified duration using default formatting.
     *
     * @param value the value to print (must not be null)
     * @return the printed result (never null)
     * @throws IllegalArgumentException if value is null
     */
    public String print(Duration value) {
        return print(value, null);
    }

    /**
     * Print the specified duration using the given unit.
     *
     * @param value the value to print (must not be null)
     * @param unit the value to use for printing (may be null to use most appropriate)
     * @return the printed result (never null)
     * @throws IllegalArgumentException if value is null
     */
    public abstract String print(Duration value, ChronoUnit unit);

    /**
     * Detect the style then parse the value to return a duration.
     * Tries SIMPLE first, then ISO8601.
     *
     * @param value the value to parse (must not be null)
     * @return the parsed duration (never null)
     * @throws IllegalArgumentException if the value is not a known style
     *                                  or cannot be parsed
     */
    public static Duration detectAndParse(String value) {
        return detectAndParse(value, null);
    }

    /**
     * Detect the style then parse the value to return a duration.
     * Pattern matching is used to determine which style to use.
     *
     * @param value the value to parse (must not be null)
     * @param unit the duration unit to use if the value doesn't specify one
     *             (null will default to milliseconds for SIMPLE style,
     *              ignored for ISO8601 style)
     * @return the parsed duration (never null)
     * @throws IllegalArgumentException if the value is not a known style
     *                                  or cannot be parsed
     */
    public static Duration detectAndParse(String value, ChronoUnit unit) {
        // Pattern matches to determine style
        // Attempts parsing with detected style
        // Throws IllegalArgumentException if no style matches
    }

    /**
     * Detect the style from the given source value.
     * Uses pattern matching to determine the format style.
     *
     * @param value the source value (must not be null)
     * @return the duration style (never null)
     * @throws IllegalArgumentException if the value is null or
     *                                  does not match any known style
     */
    public static DurationStyle detect(String value) {
        // Tests value against each style's pattern
        // Returns first matching style
        // Throws IllegalArgumentException if no match
    }

    /**
     * Check if this style matches the given value.
     *
     * @param value the value to check (must not be null)
     * @return true if this style can parse the value
     */
    protected boolean matches(String value) {
        return this.pattern.matcher(value).matches();
    }
}

PeriodStyle

Enumeration defining how period values (date-based durations) are parsed and formatted.

Thread Safety: Enums are immutable and thread-safe. All parsing and printing operations are stateless.

package org.springframework.boot.convert;

import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.regex.Pattern;

/**
 * Period format styles for parsing and printing Period values.
 *
 * Thread Safety: Immutable and thread-safe. All operations are stateless.
 *
 * @since 2.3.0
 */
public enum PeriodStyle {

    /**
     * Simple formatting, for example '1y', '6m', '30d', '1y6m10d'.
     * Supports human-readable period strings with unit suffixes.
     *
     * Pattern: ^(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?$
     *
     * Supported Units (case-insensitive):
     * - y or Y = years
     * - m or M = months
     * - w or W = weeks (converted to days: 1w = 7d)
     * - d or D = days (default if no unit specified)
     *
     * Examples:
     * - "10" -> 10 days (default)
     * - "1y" -> 1 year
     * - "6m" -> 6 months
     * - "2w" -> 14 days (2 weeks)
     * - "30d" -> 30 days
     * - "1y6m10d" -> 1 year, 6 months, 10 days
     * - "-1y" -> negative 1 year
     */
    SIMPLE("^(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?$",
           Pattern.CASE_INSENSITIVE) {

        @Override
        public Period parse(String value, ChronoUnit unit) {
            // Parses simple format with optional unit suffixes
            // Supports combination: "1y2m3d"
            // Converts weeks to days
        }

        @Override
        public String print(Period value, ChronoUnit unit) {
            // Prints in simple format with unit suffixes
            // e.g., Period.of(1, 6, 10) -> "1y6m10d"
        }
    },

    /**
     * ISO-8601 formatting, for example 'P1Y', 'P6M', 'P1Y6M10D'.
     * Uses standard ISO-8601 period format (PnYnMnD).
     *
     * Pattern: ^[+-]?P.*$
     *
     * Format Components:
     * - P = period designator (required)
     * - nY = number of years
     * - nM = number of months
     * - nD = number of days
     *
     * Examples:
     * - "P1Y" -> 1 year
     * - "P6M" -> 6 months
     * - "P30D" -> 30 days
     * - "P1Y6M10D" -> 1 year, 6 months, 10 days
     * - "-P1Y" -> negative 1 year
     */
    ISO8601("^[+-]?P.*$", Pattern.CASE_INSENSITIVE) {

        @Override
        public Period parse(String value, ChronoUnit unit) {
            // Delegates to Java's Period.parse()
            // Ignores unit parameter (format is self-contained)
        }

        @Override
        public String print(Period value, ChronoUnit unit) {
            // Uses Java's Period.toString()
            // Returns ISO-8601 format
        }
    };

    private final Pattern pattern;

    PeriodStyle(String pattern, int flags) {
        this.pattern = Pattern.compile(pattern, flags);
    }

    /**
     * Parse the given value to a Period using the default unit (days).
     *
     * @param value the value to parse (must not be null)
     * @return a period (never null)
     * @throws IllegalArgumentException if value is null or cannot be parsed
     */
    public Period parse(String value) {
        return parse(value, null);
    }

    /**
     * Parse the given value to a period.
     *
     * @param value the value to parse (must not be null)
     * @param unit the period unit to use if the value doesn't specify one
     *             (null will default to days for SIMPLE style,
     *              ignored for ISO8601 style)
     * @return a period (never null)
     * @throws IllegalArgumentException if value is null or cannot be parsed
     * @throws java.time.format.DateTimeParseException if ISO8601 format is invalid
     */
    public abstract Period parse(String value, ChronoUnit unit);

    /**
     * Print the specified period using default formatting.
     *
     * @param value the value to print (must not be null)
     * @return the printed result (never null)
     * @throws IllegalArgumentException if value is null
     */
    public String print(Period value) {
        return print(value, null);
    }

    /**
     * Print the specified period using the given unit.
     *
     * @param value the value to print (must not be null)
     * @param unit the value to use for printing (may be null to use all components)
     * @return the printed result (never null)
     * @throws IllegalArgumentException if value is null
     */
    public abstract String print(Period value, ChronoUnit unit);

    /**
     * Detect the style then parse the value to return a period.
     * Tries SIMPLE first, then ISO8601.
     *
     * @param value the value to parse (must not be null)
     * @return the parsed period (never null)
     * @throws IllegalArgumentException if the value is not a known style
     *                                  or cannot be parsed
     */
    public static Period detectAndParse(String value) {
        return detectAndParse(value, null);
    }

    /**
     * Detect the style then parse the value to return a period.
     * Pattern matching is used to determine which style to use.
     *
     * @param value the value to parse (must not be null)
     * @param unit the period unit to use if the value doesn't specify one
     *             (null will default to days for SIMPLE style,
     *              ignored for ISO8601 style)
     * @return the parsed period (never null)
     * @throws IllegalArgumentException if the value is not a known style
     *                                  or cannot be parsed
     */
    public static Period detectAndParse(String value, ChronoUnit unit) {
        // Pattern matches to determine style
        // Attempts parsing with detected style
        // Throws IllegalArgumentException if no style matches
    }

    /**
     * Detect the style from the given source value.
     * Uses pattern matching to determine the format style.
     *
     * @param value the source value (must not be null)
     * @return the period style (never null)
     * @throws IllegalArgumentException if the value is null or
     *                                  does not match any known style
     */
    public static PeriodStyle detect(String value) {
        // Tests value against each style's pattern
        // Returns first matching style
        // Throws IllegalArgumentException if no match
    }

    /**
     * Check if this style matches the given value.
     *
     * @param value the value to check (must not be null)
     * @return true if this style can parse the value
     */
    protected boolean matches(String value) {
        return this.pattern.matcher(value).matches();
    }
}

Conversion Annotations

@DurationFormat

package org.springframework.boot.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation that can be used to indicate the format to use when
 * converting a Duration from a String.
 *
 * Used with @ConfigurationProperties and other property binding mechanisms.
 *
 * @since 2.0.0
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DurationFormat {

    /**
     * The duration format style to use for parsing.
     *
     * @return the duration format style (SIMPLE or ISO8601)
     */
    DurationStyle value();
}

@DurationUnit

package org.springframework.boot.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.temporal.ChronoUnit;

/**
 * Annotation that can be used to change the default unit used when
 * converting a Duration from a String without an explicit unit suffix.
 *
 * Without this annotation, the default unit is MILLIS.
 * With this annotation, you can specify any ChronoUnit (NANOS, MICROS,
 * MILLIS, SECONDS, MINUTES, HOURS, DAYS, etc.)
 *
 * @since 2.0.0
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DurationUnit {

    /**
     * The duration unit to use if one is not specified in the value.
     *
     * Common values:
     * - ChronoUnit.NANOS
     * - ChronoUnit.MICROS
     * - ChronoUnit.MILLIS (default without annotation)
     * - ChronoUnit.SECONDS
     * - ChronoUnit.MINUTES
     * - ChronoUnit.HOURS
     * - ChronoUnit.DAYS
     *
     * @return the duration unit (must not be null)
     */
    ChronoUnit value();
}

@PeriodFormat

package org.springframework.boot.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Annotation that can be used to indicate the format to use when
 * converting a Period from a String.
 *
 * Used with @ConfigurationProperties and other property binding mechanisms.
 *
 * @since 2.3.0
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PeriodFormat {

    /**
     * The Period format style to use for parsing.
     *
     * @return the period format style (SIMPLE or ISO8601)
     */
    PeriodStyle value();
}

@PeriodUnit

package org.springframework.boot.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.temporal.ChronoUnit;

/**
 * Annotation that can be used to change the default unit used when
 * converting a Period from a String without an explicit unit suffix.
 *
 * Without this annotation, the default unit is DAYS.
 * With this annotation, you can specify DAYS, WEEKS, MONTHS, or YEARS.
 *
 * @since 2.3.0
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PeriodUnit {

    /**
     * The Period unit to use if one is not specified in the value.
     *
     * Valid values for periods:
     * - ChronoUnit.DAYS (default without annotation)
     * - ChronoUnit.WEEKS
     * - ChronoUnit.MONTHS
     * - ChronoUnit.YEARS
     *
     * @return the Period unit (must not be null)
     */
    ChronoUnit value();
}

@DataSizeUnit

package org.springframework.boot.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.util.unit.DataUnit;

/**
 * Annotation that can be used to change the default unit used when
 * converting a DataSize from a String without an explicit unit suffix.
 *
 * Without this annotation, the default unit is BYTES.
 * With this annotation, you can specify any DataUnit (BYTES, KILOBYTES,
 * MEGABYTES, GIGABYTES, TERABYTES).
 *
 * @since 2.1.0
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSizeUnit {

    /**
     * The DataUnit to use if one is not specified in the value.
     *
     * Available units:
     * - DataUnit.BYTES (default without annotation)
     * - DataUnit.KILOBYTES
     * - DataUnit.MEGABYTES
     * - DataUnit.GIGABYTES
     * - DataUnit.TERABYTES
     *
     * @return the data unit (must not be null)
     */
    DataUnit value();
}

@Delimiter

package org.springframework.boot.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Declares a field or method parameter should be converted to a collection
 * using the specified delimiter when binding from a String.
 *
 * Without this annotation, the default delimiter is comma (,).
 * Use NONE to treat the entire string as a single collection element.
 *
 * @since 2.0.0
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.FIELD,
          ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
public @interface Delimiter {

    /**
     * A delimiter value used to indicate that no delimiter is required
     * and the result should be a single element containing the entire string.
     *
     * Use this when your values might contain the delimiter character.
     */
    String NONE = "";

    /**
     * The delimiter to use or NONE if the entire contents should be
     * treated as a single element.
     *
     * Common delimiters:
     * - "," (default without annotation)
     * - ";"
     * - "|"
     * - " " (space)
     * - NONE (no splitting)
     *
     * @return the delimiter (must not be null)
     */
    String value();
}

Complete Usage Examples

Basic Duration Conversion

import org.springframework.boot.convert.DurationStyle;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

public class DurationExample {

    public static void main(String[] args) {
        parseExamples();
        printExamples();
        errorHandling();
    }

    public static void parseExamples() {
        // Simple style (auto-detected)
        Duration d1 = DurationStyle.detectAndParse("10s");      // 10 seconds
        Duration d2 = DurationStyle.detectAndParse("5m");       // 5 minutes
        Duration d3 = DurationStyle.detectAndParse("2h");       // 2 hours
        Duration d4 = DurationStyle.detectAndParse("1d");       // 1 day
        Duration d5 = DurationStyle.detectAndParse("100");      // 100 ms (default)
        Duration d6 = DurationStyle.detectAndParse("500ms");    // 500 milliseconds
        Duration d7 = DurationStyle.detectAndParse("10ns");     // 10 nanoseconds
        Duration d8 = DurationStyle.detectAndParse("-10s");     // negative 10 seconds

        // ISO-8601 style (auto-detected)
        Duration d9 = DurationStyle.detectAndParse("PT10S");    // 10 seconds
        Duration d10 = DurationStyle.detectAndParse("PT5M");    // 5 minutes
        Duration d11 = DurationStyle.detectAndParse("PT2H30M"); // 2h 30m
        Duration d12 = DurationStyle.detectAndParse("P2D");     // 2 days

        // Explicit style
        Duration d13 = DurationStyle.SIMPLE.parse("30s");
        Duration d14 = DurationStyle.ISO8601.parse("PT30S");

        // With default unit (overrides default milliseconds)
        Duration d15 = DurationStyle.detectAndParse("10", ChronoUnit.SECONDS);
        // Parses "10" as 10 seconds instead of 10 milliseconds

        Duration d16 = DurationStyle.detectAndParse("5", ChronoUnit.MINUTES);
        // Parses "5" as 5 minutes

        System.out.println("d1 (10s): " + d1.getSeconds() + " seconds");
        System.out.println("d15 (10 with SECONDS unit): " + d15.getSeconds() + " seconds");
    }

    public static void printExamples() {
        Duration duration = Duration.ofMinutes(5);

        // Simple style
        String s1 = DurationStyle.SIMPLE.print(duration);
        System.out.println("Simple: " + s1);  // "5m"

        // ISO-8601 style
        String s2 = DurationStyle.ISO8601.print(duration);
        System.out.println("ISO-8601: " + s2); // "PT5M"

        // With specific unit
        String s3 = DurationStyle.SIMPLE.print(duration, ChronoUnit.SECONDS);
        System.out.println("As seconds: " + s3); // "300s"

        // Complex duration
        Duration complex = Duration.ofHours(2).plusMinutes(30).plusSeconds(15);
        System.out.println("Complex SIMPLE: " + DurationStyle.SIMPLE.print(complex));
        System.out.println("Complex ISO8601: " + DurationStyle.ISO8601.print(complex));
    }

    public static void errorHandling() {
        try {
            // Invalid format
            Duration invalid = DurationStyle.detectAndParse("invalid");
        } catch (IllegalArgumentException e) {
            System.err.println("Parse error: " + e.getMessage());
        }

        try {
            // Wrong style
            Duration wrong = DurationStyle.SIMPLE.parse("PT10S");
        } catch (IllegalArgumentException e) {
            System.err.println("Style mismatch: " + e.getMessage());
        }
    }
}

Configuration Properties with Durations

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.convert.DurationFormat;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

@ConfigurationProperties("app.timeout")
@Validated
@Component
@EnableConfigurationProperties
public class TimeoutProperties {

    /**
     * Connection timeout.
     * Default: accepts "30s", "30000ms", "PT30S"
     * Default unit: milliseconds
     */
    @NotNull
    private Duration connection = Duration.ofSeconds(30);

    /**
     * Read timeout with seconds as default unit.
     * "30" parsed as 30 seconds (not 30 milliseconds)
     */
    @DurationUnit(ChronoUnit.SECONDS)
    @Min(1)
    private Duration read = Duration.ofSeconds(10);

    /**
     * Write timeout - only accepts ISO-8601 format.
     * Must be specified as "PT1M", "PT30S", etc.
     */
    @DurationFormat(DurationStyle.ISO8601)
    @NotNull
    private Duration write = Duration.ofSeconds(30);

    /**
     * Idle timeout - combination of seconds as default unit
     * with simple format enforced.
     */
    @DurationUnit(ChronoUnit.SECONDS)
    @DurationFormat(DurationStyle.SIMPLE)
    private Duration idle = Duration.ofMinutes(5);

    // Getters and setters with validation
    public Duration getConnection() {
        return connection;
    }

    public void setConnection(Duration connection) {
        if (connection.isNegative()) {
            throw new IllegalArgumentException("Connection timeout must be positive");
        }
        this.connection = connection;
    }

    public Duration getRead() {
        return read;
    }

    public void setRead(Duration read) {
        this.read = read;
    }

    public Duration getWrite() {
        return write;
    }

    public void setWrite(Duration write) {
        this.write = write;
    }

    public Duration getIdle() {
        return idle;
    }

    public void setIdle(Duration idle) {
        this.idle = idle;
    }
}
# application.yml
app:
  timeout:
    connection: 30s              # 30 seconds (simple format)
    read: 10                     # 10 seconds (due to @DurationUnit)
    write: PT1M                  # 1 minute (ISO-8601 required)
    idle: 300                    # 300 seconds (due to @DurationUnit)
# application.properties
app.timeout.connection=30s
app.timeout.read=10
app.timeout.write=PT1M
app.timeout.idle=300

Period Conversion

import org.springframework.boot.convert.PeriodStyle;
import java.time.Period;
import java.time.temporal.ChronoUnit;

public class PeriodExample {

    public static void main(String[] args) {
        parseExamples();
        printExamples();
        businessLogicExample();
    }

    public static void parseExamples() {
        // Simple style (case-insensitive)
        Period p1 = PeriodStyle.detectAndParse("1y");       // 1 year
        Period p2 = PeriodStyle.detectAndParse("6M");       // 6 months
        Period p3 = PeriodStyle.detectAndParse("30d");      // 30 days
        Period p4 = PeriodStyle.detectAndParse("2w");       // 2 weeks (14 days)
        Period p5 = PeriodStyle.detectAndParse("1Y6M10D");  // 1 year 6 months 10 days
        Period p6 = PeriodStyle.detectAndParse("90");       // 90 days (default)
        Period p7 = PeriodStyle.detectAndParse("-1y");      // negative 1 year

        // ISO-8601 style
        Period p8 = PeriodStyle.detectAndParse("P1Y");      // 1 year
        Period p9 = PeriodStyle.detectAndParse("P6M");      // 6 months
        Period p10 = PeriodStyle.detectAndParse("P1Y6M10D"); // 1 year 6 months 10 days

        // Explicit style
        Period p11 = PeriodStyle.SIMPLE.parse("1y");
        Period p12 = PeriodStyle.ISO8601.parse("P1Y");

        // With default unit
        Period p13 = PeriodStyle.detectAndParse("90", ChronoUnit.DAYS);
        Period p14 = PeriodStyle.detectAndParse("12", ChronoUnit.MONTHS);

        System.out.println("p1 years: " + p1.getYears());
        System.out.println("p5: " + p5.getYears() + "y " +
                          p5.getMonths() + "m " + p5.getDays() + "d");
    }

    public static void printExamples() {
        Period period = Period.of(1, 6, 10);  // 1y 6m 10d

        // Simple style
        String s1 = PeriodStyle.SIMPLE.print(period);
        System.out.println("Simple: " + s1);  // "1y6m10d"

        // ISO-8601 style
        String s2 = PeriodStyle.ISO8601.print(period);
        System.out.println("ISO-8601: " + s2); // "P1Y6M10D"

        // Period with only days
        Period daysOnly = Period.ofDays(90);
        System.out.println("90 days simple: " + PeriodStyle.SIMPLE.print(daysOnly));
        System.out.println("90 days ISO: " + PeriodStyle.ISO8601.print(daysOnly));
    }

    public static void businessLogicExample() {
        // Data retention policy
        Period retentionPeriod = PeriodStyle.detectAndParse("1y");

        java.time.LocalDate today = java.time.LocalDate.now();
        java.time.LocalDate expirationDate = today.minus(retentionPeriod);

        System.out.println("Data older than " + expirationDate + " should be archived");
    }
}

Delimited String Conversion

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.Delimiter;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
import java.util.ArrayList;

@ConfigurationProperties("app")
@Component
public class DelimitedProperties {

    /**
     * Tags - default delimiter is comma.
     * "tag1,tag2,tag3" -> ["tag1", "tag2", "tag3"]
     */
    private List<String> tags = new ArrayList<>();

    /**
     * Hosts - custom semicolon delimiter.
     * "host1;host2;host3" -> ["host1", "host2", "host3"]
     */
    @Delimiter(";")
    private List<String> hosts = new ArrayList<>();

    /**
     * Ports - pipe delimiter with Set to remove duplicates.
     * "8080|8443|9000" -> [8080, 8443, 9000]
     */
    @Delimiter("|")
    private Set<Integer> ports;

    /**
     * No delimiter - entire value as single element.
     * "a,b,c" -> ["a,b,c"] (not split)
     */
    @Delimiter(Delimiter.NONE)
    private List<String> description = new ArrayList<>();

    /**
     * Space-delimited list.
     * "value1 value2 value3" -> ["value1", "value2", "value3"]
     */
    @Delimiter(" ")
    private List<String> keywords = new ArrayList<>();

    // Getters and setters
    public List<String> getTags() {
        return tags;
    }

    public void setTags(List<String> tags) {
        this.tags = tags;
    }

    public List<String> getHosts() {
        return hosts;
    }

    public void setHosts(List<String> hosts) {
        this.hosts = hosts;
    }

    public Set<Integer> getPorts() {
        return ports;
    }

    public void setPorts(Set<Integer> ports) {
        this.ports = ports;
    }

    public List<String> getDescription() {
        return description;
    }

    public void setDescription(List<String> description) {
        this.description = description;
    }

    public List<String> getKeywords() {
        return keywords;
    }

    public void setKeywords(List<String> keywords) {
        this.keywords = keywords;
    }
}
# application.yml
app:
  tags: production,backend,critical
  hosts: server1.example.com;server2.example.com;server3.example.com
  ports: 8080|8443|9000
  description: This is a single value, not split on commas
  keywords: java spring microservices cloud

Using ApplicationConversionService

import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
import java.time.Duration;
import java.time.Period;
import java.util.List;

@Component
public class ConversionExample {

    private final ConversionService conversionService;

    public ConversionExample() {
        // Use shared instance (recommended for most cases)
        this.conversionService = ApplicationConversionService.getSharedInstance();
    }

    public void convertExamples() {
        // String to Duration
        Duration d1 = conversionService.convert("30s", Duration.class);
        System.out.println("Duration: " + d1.getSeconds() + " seconds");

        // String to Period
        Period p1 = conversionService.convert("90d", Period.class);
        System.out.println("Period: " + p1.getDays() + " days");

        // String to DataSize
        DataSize size = conversionService.convert("10MB", DataSize.class);
        System.out.println("DataSize: " + size.toMegabytes() + " MB");

        // Delimited string to List (requires TypeDescriptor)
        TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class);
        TypeDescriptor targetType = TypeDescriptor.collection(List.class,
                                                              TypeDescriptor.valueOf(String.class));

        @SuppressWarnings("unchecked")
        List<String> list = (List<String>) conversionService.convert(
            "a,b,c",
            sourceType,
            targetType
        );
        System.out.println("List: " + list);

        // Check if conversion is possible
        boolean canConvert = conversionService.canConvert(
            String.class,
            Duration.class
        );
        System.out.println("Can convert String to Duration: " + canConvert);
    }

    public void errorHandlingExample() {
        try {
            // Invalid duration format
            Duration invalid = conversionService.convert("not-a-duration", Duration.class);
        } catch (Exception e) {
            System.err.println("Conversion error: " + e.getMessage());
            // Handle error appropriately
        }
    }
}

Custom Converter Registration

import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

@Configuration
public class ConversionConfiguration {

    /**
     * Create custom conversion service with additional converters.
     */
    @Bean
    public ConversionService conversionService() {
        ApplicationConversionService service = new ApplicationConversionService();

        // Add custom converters
        service.addConverter(new StringToEmailConverter());
        service.addConverter(new StringToPhoneNumberConverter());
        service.addConverter(new StringToUrlConverter());

        return service;
    }

    /**
     * Custom converter for email addresses
     */
    public static class StringToEmailConverter implements Converter<String, Email> {

        private static final Pattern EMAIL_PATTERN = Pattern.compile(
            "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"
        );

        @Override
        public Email convert(String source) {
            if (source == null || source.trim().isEmpty()) {
                return null;
            }

            source = source.trim().toLowerCase();

            if (!EMAIL_PATTERN.matcher(source).matches()) {
                throw new IllegalArgumentException(
                    "Invalid email format: " + source
                );
            }

            return new Email(source);
        }
    }

    /**
     * Custom converter for phone numbers
     */
    public static class StringToPhoneNumberConverter implements Converter<String, PhoneNumber> {

        private static final Pattern PHONE_PATTERN = Pattern.compile(
            "^\\+?[1-9]\\d{1,14}$"
        );

        @Override
        public PhoneNumber convert(String source) {
            if (source == null || source.trim().isEmpty()) {
                return null;
            }

            // Remove common separators
            String cleaned = source.replaceAll("[\\s()-]", "");

            if (!PHONE_PATTERN.matcher(cleaned).matches()) {
                throw new IllegalArgumentException(
                    "Invalid phone number format: " + source
                );
            }

            return new PhoneNumber(cleaned);
        }
    }

    /**
     * Custom converter for URLs with validation
     */
    public static class StringToUrlConverter implements Converter<String, java.net.URL> {

        @Override
        public java.net.URL convert(String source) {
            if (source == null || source.trim().isEmpty()) {
                return null;
            }

            try {
                return new java.net.URL(source);
            } catch (java.net.MalformedURLException e) {
                throw new IllegalArgumentException(
                    "Invalid URL: " + source, e
                );
            }
        }
    }

    /**
     * Alternative: Configure existing conversion service via customizer
     */
    @Bean
    public ConversionServiceConfigurer conversionServiceConfigurer() {
        return registry -> {
            // Add standard Spring Boot converters
            ApplicationConversionService.addApplicationConverters(registry);

            // Add custom converters
            registry.addConverter(new StringToEmailConverter());
            registry.addConverter(new StringToPhoneNumberConverter());
        };
    }

    @FunctionalInterface
    public interface ConversionServiceConfigurer {
        void configure(FormatterRegistry registry);
    }
}

// Supporting value objects
class Email {
    private final String address;

    public Email(String address) {
        this.address = address;
    }

    public String getAddress() {
        return address;
    }
}

class PhoneNumber {
    private final String number;

    public PhoneNumber(String number) {
        this.number = number;
    }

    public String getNumber() {
        return number;
    }
}

DataSize Conversion

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
import javax.validation.constraints.NotNull;

@ConfigurationProperties("app.storage")
@Component
public class StorageProperties {

    /**
     * Cache size - default unit is bytes.
     * "10MB" or "10485760" (bytes)
     */
    @NotNull
    private DataSize cacheSize = DataSize.ofMegabytes(256);

    /**
     * Max upload size - default unit is megabytes.
     * "10" parsed as 10MB
     */
    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize maxUploadSize = DataSize.ofMegabytes(50);

    /**
     * Disk quota - default unit is gigabytes.
     * "100" parsed as 100GB
     */
    @DataSizeUnit(DataUnit.GIGABYTES)
    private DataSize diskQuota = DataSize.ofTerabytes(1);

    /**
     * Buffer size - default unit is kilobytes.
     * "64" parsed as 64KB
     */
    @DataSizeUnit(DataUnit.KILOBYTES)
    private DataSize bufferSize = DataSize.ofKilobytes(64);

    // Getters and setters with validation
    public DataSize getCacheSize() {
        return cacheSize;
    }

    public void setCacheSize(DataSize cacheSize) {
        if (cacheSize.toBytes() <= 0) {
            throw new IllegalArgumentException("Cache size must be positive");
        }
        this.cacheSize = cacheSize;
    }

    public DataSize getMaxUploadSize() {
        return maxUploadSize;
    }

    public void setMaxUploadSize(DataSize maxUploadSize) {
        this.maxUploadSize = maxUploadSize;
    }

    public DataSize getDiskQuota() {
        return diskQuota;
    }

    public void setDiskQuota(DataSize diskQuota) {
        this.diskQuota = diskQuota;
    }

    public DataSize getBufferSize() {
        return bufferSize;
    }

    public void setBufferSize(DataSize bufferSize) {
        this.bufferSize = bufferSize;
    }

    // Utility methods
    public boolean hasEnoughDiskSpace(DataSize required) {
        return diskQuota.toBytes() >= required.toBytes();
    }

    public boolean isWithinUploadLimit(long fileSizeBytes) {
        return fileSizeBytes <= maxUploadSize.toBytes();
    }
}
# application.yml
app:
  storage:
    cache-size: 256MB           # Explicit unit
    max-upload-size: 50         # 50MB (due to @DataSizeUnit)
    disk-quota: 1TB             # Explicit unit
    buffer-size: 64             # 64KB (due to @DataSizeUnit)

Error Handling

Parsing Errors

import org.springframework.boot.convert.DurationStyle;
import org.springframework.boot.convert.PeriodStyle;
import java.time.Duration;
import java.time.Period;
import java.time.format.DateTimeParseException;

public class ConversionErrorHandling {

    public static Duration parseDurationSafely(String value, Duration defaultValue) {
        try {
            return DurationStyle.detectAndParse(value);
        } catch (IllegalArgumentException e) {
            System.err.println("Invalid duration format: " + value);
            System.err.println("Error: " + e.getMessage());
            return defaultValue;
        } catch (DateTimeParseException e) {
            System.err.println("Invalid ISO-8601 duration: " + value);
            System.err.println("Error: " + e.getMessage());
            return defaultValue;
        }
    }

    public static Period parsePeriodSafely(String value, Period defaultValue) {
        try {
            return PeriodStyle.detectAndParse(value);
        } catch (IllegalArgumentException e) {
            System.err.println("Invalid period format: " + value);
            System.err.println("Error: " + e.getMessage());
            return defaultValue;
        } catch (DateTimeParseException e) {
            System.err.println("Invalid ISO-8601 period: " + value);
            System.err.println("Error: " + e.getMessage());
            return defaultValue;
        }
    }

    public static void main(String[] args) {
        // Valid parsing
        Duration d1 = parseDurationSafely("30s", Duration.ofSeconds(10));
        System.out.println("Parsed: " + d1.getSeconds() + "s");

        // Invalid parsing - uses default
        Duration d2 = parseDurationSafely("invalid", Duration.ofSeconds(10));
        System.out.println("Default: " + d2.getSeconds() + "s");

        // Style detection failure
        Duration d3 = parseDurationSafely("notaduration", Duration.ZERO);
        System.out.println("Fallback: " + d3.getSeconds() + "s");
    }
}

Validation with Configuration Properties

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

@ConfigurationProperties("app.timeouts")
@Validated
@Component
public class ValidatedTimeoutProperties {

    @NotNull(message = "Connection timeout must be specified")
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration connection;

    @Positive(message = "Read timeout must be positive")
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration read = Duration.ofSeconds(30);

    // Custom validation
    public void setConnection(Duration connection) {
        if (connection != null && connection.toSeconds() > 300) {
            throw new IllegalArgumentException(
                "Connection timeout cannot exceed 300 seconds"
            );
        }
        this.connection = connection;
    }

    public Duration getConnection() {
        return connection;
    }

    public Duration getRead() {
        return read;
    }

    public void setRead(Duration read) {
        this.read = read;
    }
}

Common Patterns

Multi-Tenant Configuration

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.stereotype.Component;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Map;
import java.util.HashMap;

@ConfigurationProperties("tenants")
@Component
public class MultiTenantProperties {

    private Map<String, TenantConfig> configs = new HashMap<>();

    public static class TenantConfig {

        @DurationUnit(ChronoUnit.SECONDS)
        private Duration sessionTimeout = Duration.ofMinutes(30);

        @DataSizeUnit(DataUnit.MEGABYTES)
        private DataSize maxStoragePerUser = DataSize.ofGigabytes(1);

        private int maxConcurrentUsers = 100;

        // Getters and setters
        public Duration getSessionTimeout() {
            return sessionTimeout;
        }

        public void setSessionTimeout(Duration sessionTimeout) {
            this.sessionTimeout = sessionTimeout;
        }

        public DataSize getMaxStoragePerUser() {
            return maxStoragePerUser;
        }

        public void setMaxStoragePerUser(DataSize maxStoragePerUser) {
            this.maxStoragePerUser = maxStoragePerUser;
        }

        public int getMaxConcurrentUsers() {
            return maxConcurrentUsers;
        }

        public void setMaxConcurrentUsers(int maxConcurrentUsers) {
            this.maxConcurrentUsers = maxConcurrentUsers;
        }
    }

    public Map<String, TenantConfig> getConfigs() {
        return configs;
    }

    public void setConfigs(Map<String, TenantConfig> configs) {
        this.configs = configs;
    }

    public TenantConfig getConfigForTenant(String tenantId) {
        return configs.getOrDefault(tenantId, new TenantConfig());
    }
}
# application.yml
tenants:
  configs:
    tenant-a:
      session-timeout: 1800  # 30 minutes (seconds)
      max-storage-per-user: 2048  # 2GB (megabytes)
      max-concurrent-users: 200
    tenant-b:
      session-timeout: 3600  # 1 hour (seconds)
      max-storage-per-user: 5120  # 5GB (megabytes)
      max-concurrent-users: 500

Feature Flags with Durations

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

@ConfigurationProperties("features")
@Component
public class FeatureFlagProperties {

    private FeatureConfig newDashboard = new FeatureConfig();
    private FeatureConfig betaApi = new FeatureConfig();

    public static class FeatureConfig {

        private boolean enabled = false;

        @DurationUnit(ChronoUnit.DAYS)
        private Duration rolloutPeriod = Duration.ofDays(14);

        private Instant startDate;

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public Duration getRolloutPeriod() {
            return rolloutPeriod;
        }

        public void setRolloutPeriod(Duration rolloutPeriod) {
            this.rolloutPeriod = rolloutPeriod;
        }

        public Instant getStartDate() {
            return startDate;
        }

        public void setStartDate(Instant startDate) {
            this.startDate = startDate;
        }

        public boolean isInRolloutPeriod() {
            if (!enabled || startDate == null) {
                return false;
            }

            Instant now = Instant.now();
            Instant endDate = startDate.plus(rolloutPeriod);

            return now.isAfter(startDate) && now.isBefore(endDate);
        }
    }

    public FeatureConfig getNewDashboard() {
        return newDashboard;
    }

    public void setNewDashboard(FeatureConfig newDashboard) {
        this.newDashboard = newDashboard;
    }

    public FeatureConfig getBetaApi() {
        return betaApi;
    }

    public void setBetaApi(FeatureConfig betaApi) {
        this.betaApi = betaApi;
    }
}

Best Practices

1. Use Appropriate Annotations

// GOOD: Clear default unit specification
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout;  // "30" -> 30 seconds

// AVOID: Relying on implicit millisecond default
private Duration timeout;  // "30" -> 30 milliseconds (confusing!)

// GOOD: Explicit format requirement
@DurationFormat(DurationStyle.ISO8601)
private Duration apiTimeout;

// GOOD: Appropriate data size units
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize maxUploadSize;  // "50" -> 50MB

2. Choose the Right Style

// For user-facing configuration: SIMPLE (more readable)
@DurationFormat(DurationStyle.SIMPLE)
private Duration userTimeout;  // Users write "30s", "5m"

// For API/interoperability: ISO8601 (standardized)
@DurationFormat(DurationStyle.ISO8601)
private Duration apiTimeout;   // Systems write "PT30S", "PT5M"

// For internal configuration: Allow both (auto-detect)
private Duration internalTimeout;  // Accepts both formats

3. Delimiter Selection

// Use comma for simple lists (default)
@Delimiter(",")
private List<String> tags;  // "tag1,tag2,tag3"

// Use different delimiter if values contain commas
@Delimiter(";")
private List<String> descriptions;  // "Description 1, with comma;Description 2"

// Use NONE if splitting is not desired
@Delimiter(Delimiter.NONE)
private List<String> singleValue;  // "a,b,c" -> ["a,b,c"]

// Use space for keyword lists
@Delimiter(" ")
private List<String> keywords;  // "java spring boot"

4. Type Safety with DataSize

// GOOD: Type-safe data sizes with automatic unit conversion
private DataSize maxSize;
long bytes = maxSize.toBytes();
long megabytes = maxSize.toMegabytes();

// AVOID: Using primitives for sizes (error-prone)
private long maxSizeBytes;  // What unit? Bytes? KB? MB?

5. Validation

@ConfigurationProperties("app")
@Validated
public class AppProperties {

    @DurationUnit(ChronoUnit.SECONDS)
    @Min(1)
    @Max(3600)
    private Duration timeout;  // Must be between 1 and 3600 seconds

    @NotNull
    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize maxUploadSize;

    @NotEmpty
    @Delimiter(";")
    private List<String> allowedHosts;

    // Custom validation in setter
    public void setTimeout(Duration timeout) {
        if (timeout.isNegative() || timeout.isZero()) {
            throw new IllegalArgumentException("Timeout must be positive");
        }
        this.timeout = timeout;
    }
}

6. Shared Conversion Service

// GOOD: Use shared instance (thread-safe, cached)
ConversionService service = ApplicationConversionService.getSharedInstance();

// AVOID: Creating new instances repeatedly
ConversionService service = new ApplicationConversionService();  // Wasteful

7. Custom Converters

// GOOD: Implement Converter interface with null safety
public class StringToEmailConverter implements Converter<String, Email> {
    @Override
    public Email convert(String source) {
        if (source == null || source.trim().isEmpty()) {
            return null;  // Handle null/empty gracefully
        }
        // Validation and conversion logic
        return new Email(source);
    }
}

// Register globally or in specific conversion service

8. Documentation

/**
 * Connection timeout configuration.
 *
 * Format: Simple duration format ("30s", "5m", "2h")
 * Default unit: seconds (if no suffix provided)
 * Default value: 30 seconds
 * Valid range: 1-300 seconds
 *
 * Examples:
 * - "30s" or "30" -> 30 seconds
 * - "5m" -> 5 minutes
 * - "PT1M" -> 1 minute (ISO-8601)
 */
@DurationUnit(ChronoUnit.SECONDS)
@Min(1)
@Max(300)
private Duration connectionTimeout = Duration.ofSeconds(30);

Format Reference

Duration Formats

InputDefault UnitParsed AsNotes
"30"MILLISECONDS30 millisecondsDefault without @DurationUnit
"10ns"-10 nanosecondsExplicit unit
"10us"-10 microsecondsExplicit unit
"30ms"-30 millisecondsExplicit unit
"30s"-30 secondsExplicit unit
"5m"-5 minutesExplicit unit
"2h"-2 hoursExplicit unit
"1d"-1 dayExplicit unit
"-10s"--10 secondsNegative duration
"PT30S"-30 secondsISO-8601 format
"PT5M"-5 minutesISO-8601 format
"PT2H30M"-2h 30mISO-8601 format
"P2D"-2 daysISO-8601 format

Period Formats

InputDefault UnitParsed AsNotes
"90"DAYS90 daysDefault without @PeriodUnit
"1y"-1 yearCase-insensitive
"1Y"-1 yearCase-insensitive
"6m"-6 monthsCase-insensitive
"6M"-6 monthsCase-insensitive
"2w"-14 daysWeeks to days
"2W"-14 daysWeeks to days
"30d"-30 daysCase-insensitive
"30D"-30 daysCase-insensitive
"1y6m10d"-1y 6m 10dCombined format
"1Y6M10D"-1y 6m 10dCombined format
"P1Y"-1 yearISO-8601 format
"P6M"-6 monthsISO-8601 format
"P1Y6M10D"-1y 6m 10dISO-8601 format

DataSize Formats

InputDefault UnitParsed AsNotes
"1024"BYTES1024 bytesDefault without @DataSizeUnit
"1KB"-1 kilobyte1024 bytes
"10MB"-10 megabytes10 × 1024² bytes
"5GB"-5 gigabytes5 × 1024³ bytes
"1TB"-1 terabyte1 × 1024⁴ bytes

Thread Safety

ApplicationConversionService:

  • Thread-safe for all read operations (conversions)
  • The shared instance is safely published using double-checked locking
  • Should not be modified (add converters) after construction in multi-threaded environments
  • Immutable after construction

DurationStyle and PeriodStyle:

  • Enums are immutable and inherently thread-safe
  • All parsing and printing operations are stateless
  • Can be used concurrently without synchronization

Conversion Operations:

  • All Spring Boot converters are stateless
  • Safe to invoke from multiple threads simultaneously
  • No shared mutable state

Custom Converters:

  • Must be implemented as thread-safe (stateless)
  • Avoid instance fields that change during conversion
  • If caching is needed, use thread-safe collections

Related Components

  • ConfigurationProperties: Uses ApplicationConversionService for property binding
  • Binder: Leverages conversion service for type conversion during binding
  • Spring Framework ConversionService: Base conversion infrastructure
  • Property Binding: Automatically applies conversion annotations during binding
  • Environment: Uses conversion service for property value conversion
  • @Value: Can leverage custom converters for injection

Integration with Spring Boot Features

Auto-Configuration

// ApplicationConversionService is auto-configured
// Available for injection in Spring Boot applications

@Component
public class MyService {

    private final ConversionService conversionService;

    // Autowired - uses ApplicationConversionService
    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void processConfig(String durationStr) {
        Duration duration = conversionService.convert(durationStr, Duration.class);
        // Use duration
    }
}

Custom Converter Auto-Registration

// Converters implementing Spring's Converter interface
// are automatically discovered and registered

@Component
public class MyCustomConverter implements Converter<String, MyType> {

    @Override
    public MyType convert(String source) {
        // Conversion logic
        return new MyType(source);
    }
}

// Automatically registered with ApplicationConversionService

Common Pitfalls

1. Missing Default Unit - Millisecond Trap

Problem: Forgetting that durations without a unit default to milliseconds, not seconds.

// WRONG - thinks it's 30 seconds, but it's 30 milliseconds!
@ConfigurationProperties("app")
public class AppConfig {
    private Duration timeout = Duration.ofSeconds(30);
    // In application.yml: timeout: 30
    // Actual value: 30ms (not 30s!)
}

Error: Application times out immediately, connections fail rapidly.

Solution: Always use @DurationUnit or include units in config:

// CORRECT - explicit unit annotation
@ConfigurationProperties("app")
public class AppConfig {
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration timeout;  // "30" now means 30 seconds
}

// Or in configuration - explicit unit
// application.yml:  timeout: 30s

Why It Happens: Spring Boot's default is milliseconds for historical reasons. Most developers expect seconds.

2. ISO8601 Format Confusion

Problem: Mixing simple and ISO8601 format styles causes parsing errors.

@ConfigurationProperties("app")
public class AppConfig {
    @DurationFormat(DurationStyle.ISO8601)  // Expects "PT30S"
    private Duration timeout;
}

// application.yml
app:
  timeout: 30s  // WRONG - simple format but ISO8601 expected

Error:

IllegalArgumentException: Could not parse duration '30s'

Solution: Match format style with configuration format:

// Option 1: Use SIMPLE format (most user-friendly)
@DurationFormat(DurationStyle.SIMPLE)
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout;  // Accepts "30", "30s", "1m"

// Option 2: Use ISO8601 format (standards-compliant)
@DurationFormat(DurationStyle.ISO8601)
private Duration timeout;  // Requires "PT30S", "PT1M"

// Option 3: Let auto-detection work (recommended)
private Duration timeout;  // Accepts both formats

Best Practice: Omit @DurationFormat to support both formats automatically.

3. Period vs Duration Confusion

Problem: Using Period for time-based durations or Duration for date-based periods.

// WRONG - Period doesn't support hours/minutes/seconds
@ConfigurationProperties("app")
public class AppConfig {
    private Period cacheExpiry;  // Can't represent "30 minutes"
}

// application.yml - this will FAIL
app:
  cache-expiry: 30m  // Period doesn't support minutes!

Error:

IllegalArgumentException: Period cannot be parsed from duration

Solution: Use the right type for the time scale:

// Use Duration for time-based values (seconds, minutes, hours, days)
private Duration cacheExpiry;      // For "30m", "2h", "1d"
private Duration sessionTimeout;   // For "15m"
private Duration requestTimeout;   // For "30s"

// Use Period for date-based values (days, months, years)
private Period retentionPeriod;    // For "90d", "6m", "1y"
private Period archivePeriod;      // For "7d", "1m"

Rule of Thumb:

  • Duration: Precise time (PT prefix) - hours, minutes, seconds
  • Period: Calendar time (P prefix) - years, months, days

4. DataSize Unit Ambiguity - KB vs KiB

Problem: Confusion between decimal (1000-based) and binary (1024-based) units.

@ConfigurationProperties("app")
public class AppConfig {
    private DataSize maxUploadSize;
}

// application.yml
app:
  max-upload-size: 10KB  // Is this 10,000 or 10,240 bytes?

Solution: Spring Boot uses decimal units (1000-based):

// Spring Boot DataSize units (all 1000-based)
10KB = 10,000 bytes
10MB = 10,000,000 bytes
10GB = 10,000,000,000 bytes

// NOT binary units
10KB ≠ 10,240 bytes (10 KiB)
10MB ≠ 10,485,760 bytes (10 MiB)

Best Practice: Be explicit about expectations:

@ConfigurationProperties("app")
public class AppConfig {
    @DataSizeUnit(DataUnit.MEGABYTES)  // Clear default
    private DataSize maxUploadSize;

    // Validate expectations
    @PostConstruct
    public void validate() {
        long bytes = maxUploadSize.toBytes();
        System.out.println("Max upload: " + bytes + " bytes");
    }
}

5. Delimiter Not Escaping Values

Problem: Using delimiter that appears within values causes incorrect splitting.

@ConfigurationProperties("app")
public class AppConfig {
    @Delimiter(",")
    private List<String> servers;
}

// application.yml
app:
  servers: "server1.example.com,zone=us-east,server2.example.com"
  # Expected: ["server1.example.com,zone=us-east", "server2.example.com"]
  # Actual: ["server1.example.com", "zone=us-east", "server2.example.com"]

Solution: Choose delimiter that doesn't appear in values or use array notation:

// Option 1: Different delimiter
@Delimiter(";")
private List<String> servers;
// servers: "server1.example.com,zone=us-east;server2.example.com"

// Option 2: Use YAML array (best)
@ConfigurationProperties("app")
public class AppConfig {
    private List<String> servers;  // No delimiter needed
}

// application.yml
app:
  servers:
    - "server1.example.com,zone=us-east"
    - "server2.example.com"

// Option 3: NONE delimiter for single value with commas
@Delimiter(Delimiter.NONE)
private List<String> description;  // Entire string as one element

6. ConversionService Not Injected in Tests

Problem: Unit tests fail because conversion doesn't happen.

@Test
public void testDuration() {
    AppConfig config = new AppConfig();
    config.setTimeout("30s");  // FAILS - no converter available
}

Error:

IllegalArgumentException: No converter found for String to Duration

Solution: Use proper test configuration:

// Option 1: Use @SpringBootTest
@SpringBootTest
class AppConfigTest {
    @Autowired
    private AppConfig config;  // Converters automatically available

    @Test
    void testTimeout() {
        assertThat(config.getTimeout()).isEqualTo(Duration.ofSeconds(30));
    }
}

// Option 2: Manual conversion in plain unit tests
@Test
public void testDurationParsing() {
    String input = "30s";
    Duration duration = DurationStyle.detectAndParse(input);
    assertThat(duration).isEqualTo(Duration.ofSeconds(30));
}

// Option 3: Use @ConfigurationPropertiesTest
@ConfigurationPropertiesTest
class AppConfigTest {
    @Autowired
    private AppConfig config;  // Lightweight test with converters
}

7. Negative Duration Values Misunderstood

Problem: Not realizing negative durations are valid and have specific meaning.

@ConfigurationProperties("app")
public class AppConfig {
    private Duration timeout;
}

// application.yml
app:
  timeout: -30s  // Negative duration!

Behavior: Spring Boot accepts negative durations without error.

Solution: Add validation if negative values are invalid:

@ConfigurationProperties("app")
@Validated
public class AppConfig {
    @Positive  // Only positive durations
    private Duration timeout;

    // Or custom validation
    @PostConstruct
    public void validate() {
        if (timeout.isNegative()) {
            throw new IllegalArgumentException("Timeout must be positive");
        }
    }
}

When Negative Is Valid:

  • Time zone offsets
  • Relative time calculations
  • Historical timestamps

8. ApplicationConversionService Not Used

Problem: Creating FormattingConversionService manually, missing Boot converters.

// WRONG - missing Duration, Period, DataSize converters
@Configuration
public class Config {
    @Bean
    public ConversionService conversionService() {
        return new DefaultFormattingConversionService();  // Missing Boot converters!
    }
}

Error: Properties with Duration, Period, or DataSize fail to bind.

Solution: Always use ApplicationConversionService:

// CORRECT - includes all Boot converters
@Configuration
public class Config {
    @Bean
    public ConversionService conversionService() {
        return new ApplicationConversionService();
    }

    // Or add Boot converters to existing service
    @Bean
    public ConversionService customConversionService() {
        FormattingConversionService service = new DefaultFormattingConversionService();
        ApplicationConversionService.addApplicationConverters(service);
        ApplicationConversionService.addApplicationFormatters(service);
        return service;
    }
}

9. Immutable Configuration Properties Not Working

Problem: Using Java 16+ records or Kotlin data classes, conversions fail.

// Java 17+ record
@ConfigurationProperties("app")
public record AppConfig(
    Duration timeout,  // No setter - binding fails!
    DataSize maxSize
) {}

Error:

BindException: Cannot bind to property 'timeout' - no setter available

Solution: Enable constructor binding:

// Java 17+ record - use @ConstructorBinding (or auto-detected in 3.0+)
@ConfigurationProperties("app")
@ConstructorBinding  // Explicit in Boot 2.x
public record AppConfig(
    @DurationUnit(ChronoUnit.SECONDS)
    Duration timeout,

    @DataSizeUnit(DataUnit.MEGABYTES)
    DataSize maxSize
) {}

// Or use @ConfigurationPropertiesScan
@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {}

// Kotlin data class
@ConfigurationProperties("app")
@ConstructorBinding
data class AppConfig(
    @DurationUnit(ChronoUnit.SECONDS)
    val timeout: Duration,

    @DataSizeUnit(DataUnit.MEGABYTES)
    val maxSize: DataSize
)

10. Conversion Failure Swallowed Silently

Problem: Invalid value in config causes silent failure or default value usage.

@ConfigurationProperties("app")
public class AppConfig {
    private Duration timeout = Duration.ofSeconds(30);  // Default
}

// application.yml
app:
  timeout: invalid  // WRONG value

Behavior: Application starts with default value (30s), no error logged!

Solution: Enable fail-fast with validation and explicit binding:

@ConfigurationProperties("app")
@Validated  // Enable validation
public class AppConfig {

    @NotNull  // Require non-null
    private Duration timeout;

    // Or use @PostConstruct for custom validation
    @PostConstruct
    public void validate() {
        Objects.requireNonNull(timeout, "timeout must be configured");
        if (timeout.isNegative() || timeout.isZero()) {
            throw new IllegalArgumentException("timeout must be positive");
        }
    }
}

// Enable fail-fast binding errors
// application.properties
spring.config.import-check.enabled=true

Best Practice: Always use @Validated with required properties to fail fast on startup.

Notes

  • Default Units: Always specify default units using annotations to avoid confusion
  • Format Styles: Choose between SIMPLE (user-friendly) and ISO8601 (standardized)
  • Validation: Combine with JSR-303 annotations for robust validation
  • Error Handling: Always handle IllegalArgumentException and DateTimeParseException
  • Thread Safety: All provided converters are thread-safe and stateless
  • Performance: The shared instance uses lazy initialization and caching
  • Extensibility: Easy to add custom converters via Converter interface
  • Compatibility: Fully compatible with Spring Framework's conversion infrastructure