@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:

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

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.
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:

See also