
# @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: `maxBasketSize` reads `shop.max-basket-size`. Relaxed spellings match too: `shop.maxBasketSize`, `shop.max_basket_size`, the environment variable `SHOP_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 or `SHOP_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 a `String` field keeps the value as written.

## Example

```yaml
# 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
```

```dart
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.

```dart
@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.

```dart
@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.

```dart
@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](https://github.com/farum-azula/spring_core_dart/blob/main/docs/configuration.md#completion-and-checks-in-the-editor).

## 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)

```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 `@EnableConfigurationProperties` or `@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.

## See also

- [@Value](value.md)
- [Environment](environment.md)
- [Placeholders](placeholders.md)
- [Durations](../conversion/durations.md)
- [DataSize](../conversion/data-size.md)
- [Constraints](../validation/constraints.md)
- [@Bean](../beans/bean.md)
