bind method

void bind(
  1. Map<String, Object?> values
)

Sets the target's properties from values, whose keys are property paths (name, address.city, items[0].sku); problems become field errors in bindingResult instead of exceptions.

Binding errors get Spring's arguments: {0} is the field name, which a message source may translate too. Throws a StateError when there is no target yet, an IntrospectionPartError when the class of the target is described without its setters.

// `age` is an int: the text is kept as a typeMismatch error.
binder.bind({'age': 'old'});
binder.bindingResult.fieldError('age')!.codes.first;
// typeMismatch.signup.age

Implementation

void bind(Map<String, Object?> values) {
  final target =
      _target ?? (throw StateError('No target to bind to: call construct'));
  // Without the setters every field would count as unknown and be
  // skipped: an empty object without a word is worse than an error.
  introspector
      .findIntrospection(target)
      ?.requirePart(
        IntrospectionPart.write,
        'its fields are bound from the input',
      );
  final accepted = <String, Object?>{};
  for (final MapEntry(key: raw, :value) in values.entries) {
    // Matched and bound under one canonical name, so `roles['admin']`,
    // `roles["admin"]` or `roles[ admin ]` cannot slip past a
    // `roles[admin]` rule (Spring's canonicalPropertyName).
    final key = canonicalPropertyName(raw);
    if (_isAllowed(key)) {
      accepted[key] = value;
    } else {
      _suppressed.add(key);
    }
  }
  // Required fields are checked on the accepted input only, so a
  // disallowed field also counts as missing.
  for (final field in requiredFields) {
    final value = accepted[field];
    if (value == null || (value is String && value.trim().isEmpty)) {
      // Spring's processMissingFieldError: rejected value "".
      final fixed = '${bindingResult.nestedPath}$field';
      bindingResult.recordFieldError(
        field,
        'required',
        '',
        args: _bindErrorArguments(fixed),
        defaultMessage: "Field '$fixed' is required",
      );
      accepted.remove(field);
    }
  }
  final wrapper = _wrapper(target);
  for (final MapEntry(:key, :value) in accepted.entries) {
    try {
      wrapper.setPropertyValue(key, value);
    } on TypeMismatchException catch (error) {
      _recordTypeMismatch(key, value, error);
    } on NotWritablePropertyException {
      if (!ignoreUnknownFields) {
        rethrow;
      }
    } on InvalidPropertyException {
      // A malformed key (`items[`) is an unknown field, not a crash.
      if (!ignoreUnknownFields) {
        rethrow;
      }
    } on NullValueInNestedPathException {
      if (!ignoreInvalidFields) {
        rethrow;
      }
    } on NotReadablePropertyException {
      if (!ignoreInvalidFields) {
        rethrow;
      }
    } on BeansException catch (error) {
      // Any other property access failure (e.g. a throwing setter).
      bindingResult.recordFieldError(
        key,
        'methodInvocation',
        value,
        args: _bindErrorArguments('${bindingResult.nestedPath}$key'),
        defaultMessage: error.message,
      );
    }
  }
}