Changes between Initial Version and Version 14 of Ticket #36809


Ignore:
Timestamp:
Jun 30, 2026, 1:20:35 PM (6 days ago)
Author:
Natalia Bidart
Comment:

Replying to 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).
  1. 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

Legend:

Unmodified
Added
Removed
Modified
  • Ticket #36809

    • Property Owner set to Natalia Bidart
    • Property Status newassigned
    • Property Has patch set
    • Property Keywords EmailValidator added
    • Property Triage Stage UnreviewedAccepted
    • Property Type UncategorizedNew feature
    • Property Summary Allow EmailValidator to return 'domain_part' in ValidationError paramsAllow EmailValidator to customize error messages depending on the part that failed validation
    • Property Cc Mike Edmunds jaffar Khan added
  • Ticket #36809 – Description

    initial v14  
    11Currently, 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").
    22
    3 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.
     3This 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.
    44
    55I have already prepared a patch with tests and verified that it passes all style (flake8) and functional checks.
Back to Top