#36809: Allow EmailValidator to customize error messages depending on the part that
failed validation
-------------------------------------+-------------------------------------
Reporter: Daniel E Onetti | Owner: Natalia
| Bidart
Type: New feature | Status: assigned
Component: Core (Other) | Version: dev
Severity: Normal | Resolution:
Keywords: EmailValidator | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):
* needs_better_patch: 1 => 0
* needs_docs: 1 => 0
* needs_tests: 1 => 0
* owner: jaffar Khan => Natalia Bidart
* type: Cleanup/optimization => New feature
Old description:
> Currently, the EmailValidator only provides the full 'value' in the
> params dictionary when a ValidationError is raised. However, in many use
> cases, developers need to customize error messages based specifically on
> the domain part of the email (e.g., "The domain %(domain_part)s is not
> allowed").
>
> This change adds 'domain_part' to the params dictionary in
> EmailValidator.__call__, bringing it in line with how other validators
> provide decomposed parts of the validated value. This allows for more
> granular and helpful error messages for end users.
>
> I have already prepared a patch with tests and verified that it passes
> all style (flake8) and functional checks.
New description:
Currently, the EmailValidator only provides the full 'value' in the params
dictionary when a ValidationError is raised. However, in many use cases,
developers need to customize error messages based specifically on the
domain part of the email (e.g., "The domain %(domain_part)s is not
allowed").
This change adds 'domain_part' to the `params` dictionary in
`EmailValidator.__call__`, bringing it in line with how other validators
provide decomposed parts of the validated value. This allows for more
granular and helpful error messages for end users.
I have already prepared a patch with tests and verified that it passes all
style (flake8) and functional checks.
--
Comment:
Replying to [comment:12 Mike Edmunds]:
> I couldn't tell which validators you were referring to that separate
parsing, normalization and validation.
I made this reasoning a while ago, and since you asked, I kept snoozing
the email. Sorry! But I think I meant something like this:
* `URLValidator` starts with a parsing stage (`value.split("://")` for
the scheme, `urlsplit(value)` into components), then normalizes
(`.lower()` on the scheme), then validates each piece (scheme in
`schemes`, hostname length, IPv6 in the netloc), raising as it goes.
* `DecimalValidator` also has a parsing phase (`value.as_tuple()` into
digits/exponent), then normalizes (derives `digits` / `decimals` /
`whole_digits`), then validates each against the limits, raising a
distinct code per failure from its `messages` dict.
* `FileExtensionValidator` is smaller but IMO has the same pattern:
parse+normalize (`Path(
value.name).suffix[1:].lower()`), then validate
against `allowed_extensions`.
Granted those keep the concerns inline in `__call__()` and are not split
by methods, but I think the "stages" are defined. The `BaseValidator`
family is the one that turns them into dedicated methods: `clean()` is the
normalize stage, `compare()` is the validate stage (the bool decision),
and `__call__()` owns the raise.
> [...] what you were envisioning in reworking EmailValidator to improve
subclassing hooks.
I don't think I had a specific implementation in mind when I wrote that,
but last night (and a lot of today, this was more time consuming than
anticipated, but it brought :sparkles:) I spent some time putting together
a more concrete plan. I went looking for precedents and used your use
cases as a guide (thanks for those BTW). Some thoughts:
1. **The hooks are a bool decision** A `validate_domain()` that ''only''
raises fuses the ''decision'' and the ''error'' into one method; fine for
replacing the logic, but for your `EmailValidatorWithBetterErrorMessages`
it forces exactly the wrapping you flagged. So the default hook returns a
bool and `__call__()` owns the raise. The key addition: each hook receives
the full `value`, so a subclass that ''does'' want a custom message can
override `validate_domain()` and even potentially raise a complete
`ValidationError` straight from the hook (with `value` in `params`, custom
code, etc), no wrapping. I deprecated `validate_domain_part()` in favor of
the nwe `validate_domain()` and added `validate_username()` (the names
align with Python's `email.headerregistry.Address`, per your suggestion).
2. **The default error `code` stays `"invalid"`.** There's a single
`code` for the whole validator, so today every failure raises `"invalid"`
and there's no way to give ''only'' the domain part its own code. The
`messages` dict idiom I wanted to borrow (`DecimalValidator`, and
`default_error_messages` across forms and model fields) ''does'' give each
failure its own code, but those validators were built with distinct codes
from scratch; bringing it to `EmailValidator` would mean the domain
failure starts emitting a ''new'' code instead of `"invalid"`, a
backwards-incompatible change with no clean deprecation path (I couldn't
find a precedent for changing an already-emitted code value, with or
without deprecation -- ideas welcome here!). What the new hooks ''do''
give users an opt-in: because they receive `value`, a subclass can raise
its own per-part code from `validate_domain()` (e.g.
`code="invalid_domain"`) without changing the default for everyone else.
So what's left puts the failed `username` / `domain` in `params`,
deprecates `validate_domain_part()` in favor of `validate_domain()`, and
adds `validate_username()`; both returning a bool by default but receiving
the full address:
https://github.com/django/django/pull/21568
--
Ticket URL: <
https://code.djangoproject.com/ticket/36809#comment:14>