@ConfigurationProperties
@ConfigurationProperties('prefix') turns a class into a typed bean whose fields are filled from the properties under prefix.
Why
@Value reads one key at a time. When a feature has several settings, the keys and their conversions end up spread over the code. A @ConfigurationProperties class keeps them in one place:
- each field has a type and a default;
- nested objects, lists and maps are bound from deeper keys;
- the class is a bean, so other beans inject it;
- with
@Validated(), wrong values stop the start with a message that names the key; - the generator describes the fields for completion in the editor.
The annotation makes the class a bean and gives it introspection metadata. No @Component(), @Introspected() or @EnableConfigurationProperties is needed. Binding is done by SpringApplication (spring_boot_dart and the starters).
Binding rules
- Field names. A field is bound from its name in kebab case:
maxBasketSizereadsshop.max-basket-size. Relaxed spellings match too:shop.maxBasketSize,shop.max_basket_size, the environment variableSHOP_MAXBASKETSIZE. - Defaults. A field keeps its initial value when its key is not set.
- Conversion. Values are converted to the field's type: numbers,
bool, enums,Duration(30s),DataSize(10MB),Uri, and so on. - Nested objects. A field of a class type is bound from
prefix.field.*. Classes of the same package are introspected automatically. - Lists and sets. From a comma list (
shop.currencies=EUR,USD), indexed keys (shop.hosts[0]), a YAML list orSHOP_HOSTS_0. A list comes whole from the highest source that has it. It is replaced, not merged. - Maps. From the keys below the field (
shop.prices.book=12). Entries come from every source, and a higher source wins per key. Map keys keep the form they are written in. Wrap a key with dots in brackets:shop.headers[X-Shop.Id]. - Unknown keys. Keys under the prefix that match no field are ignored, unless
ignoreUnknownFields: false. - Placeholders.
${...}in values are resolved.@RawValue()on aStringfield keeps the value as written.
Example
# lib/resources/application.yml
shop:
title: NEON Books
max-basket-size: 20
checkout-timeout: 30s
currencies: [EUR, USD]
payment:
provider: stripe
timeout: 5s
servers:
- name: eu
url: https://eu.neon.books
- name: us
url: https://us.neon.books
warehouses:
berlin:
capacity: 5000
import 'package:spring_boot_dart/spring_boot_dart.dart';
@ConfigurationProperties('shop')
class ShopProperties {
/// The name shown in the header.
String title = 'Shop';
int maxBasketSize = 10;
Duration checkoutTimeout = const Duration(minutes: 1);
List<String> currencies = ['EUR'];
Payment payment = Payment();
List<Server> servers = [];
Map<String, Warehouse> warehouses = {};
}
class Payment {
String provider = 'none';
Duration timeout = const Duration(seconds: 10);
}
class Server {
String name = '';
String url = '';
}
class Warehouse {
int capacity = 0;
}
@Service()
class Shipping {
Shipping(this.shop);
final ShopProperties shop;
int capacityOf(String city) => shop.warehouses[city]?.capacity ?? 0;
}
Immutable classes
A class whose only constructor has parameters is bound through that constructor, so its fields can be final. A parameter without a property takes its @DefaultValue, else its Dart default, else null when it is nullable. Otherwise the start fails, naming the property.
@ConfigurationProperties('shop.payment')
class PaymentProperties {
const PaymentProperties(
this.provider, {
@DefaultValue('5s') required this.timeout,
this.retries = 3,
this.webhookSecret,
});
final String provider;
final Duration timeout;
final int retries;
final String? webhookSecret;
}
Validation
With @Validated(), the constraint annotations are checked once the values are bound. A violation stops the start.
@ConfigurationProperties('shop')
@Validated()
class ShopLimits {
@Min(1)
@Max(100)
int maxBasketSize = 10;
@NotBlank()
String title = 'Shop';
}
With shop.max-basket-size=0, the failure report says:
Binding to target properties under 'shop' failed:
- shop.max-basket-size: must be greater than or equal to 1 (was "0")
In a Flutter app, @Size is exported as @SizeConstraint, because Flutter has its own Size.
On a @Bean method
The annotation also goes on a @Bean method. The object the method returns is bound, for example a class of another package.
@Configuration()
class ClientConfig {
@Bean()
@ConfigurationProperties('shop.http')
HttpSettings httpSettings() => HttpSettings();
}
Metadata for the editor
The generator writes lib/spring_gen/spring_metadata.json: every property of your classes, with its type, default and the field's /// comment as description. Enum fields list their constants. A map field is listed as headers.*, a list of classes as servers[*].url.
These annotations add to the metadata only. They check nothing at run time.
| Annotation | Meaning |
|---|---|
@Range(min, max) |
The sensible numeric bounds. Use @Min / @Max with @Validated() to check them. |
@WarnValues(values, message) |
Values that are accepted but that the editor warns about. |
@PropertyValues(values) |
The values to offer for a String field. @PropertyValues.provider('profiles') lists the profiles of the project. |
Editors with the YAML language server can complete and check application*.yml from a JSON Schema made from this metadata. See the configuration reference.
Parameters
| Name | Type | Default | Meaning |
|---|---|---|---|
prefix (positional) |
String |
required | The start of the property names, in kebab case: shop, my-app.http. |
name |
String? |
null |
The bean name. Derived from the class when null. |
ignoreInvalidFields |
bool |
false |
true keeps a field's default when its value cannot be converted, instead of failing the start. |
ignoreUnknownFields |
bool |
true |
false fails the start for keys under the prefix that match no field, which catches typos. OS environment variables are not checked. |
Related annotations
| Annotation | Where | Meaning |
|---|---|---|
@Name('import') |
field, constructor parameter | Binds from another property name. Needed when the key is a Dart keyword. |
@DefaultValue('5s') |
constructor parameter | The default, written as a property value. @DefaultValue() gives an empty collection or a nested object with its own defaults. |
@DurationUnit(ChronoUnit.seconds) |
Duration field |
The unit of a plain number. Milliseconds otherwise. |
@DataSizeUnit(DataUnit.megabytes) |
DataSize field |
The unit of a plain number. Bytes otherwise. |
@RawValue() |
String field |
The value is bound as written, without resolving ${...}. |
In Spring (Java)
@ConfigurationProperties("shop")
@Validated
public class ShopProperties {
@Min(1) @Max(100) private int maxBasketSize = 10;
private Payment payment = new Payment();
// getters and setters
}
@ConfigurationProperties("shop.payment")
public record PaymentProperties(String provider,
@DefaultValue("5s") Duration timeout) {}
Differences that matter:
- No
@EnableConfigurationPropertiesor@ConfigurationPropertiesScan. The annotation makes the class a bean. - Fields are bound directly. No getters and setters are needed.
- The metadata comes from the code only. There is no hand-written additional metadata file.