bind method
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.
- Keys not allowed by allowedFields and disallowedFields are skipped and listed in suppressedFields. Keys are compared in a canonical form (see canonicalPropertyName).
- A missing or blank entry of requiredFields gets a
requirederror with the rejected value"". - A value that cannot be converted gets a
typeMismatcherror, and a setter that throws amethodInvocationerror. - Unknown fields and malformed keys are skipped while ignoreUnknownFields is on; unreachable nested paths throw unless ignoreInvalidFields is on.
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,
);
}
}
}