Skip to content

Commit 1b32654

Browse files
committed
feat: EmailOptions for configuring email format validation
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent d938d78 commit 1b32654

File tree

7 files changed

+197
-21
lines changed

7 files changed

+197
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `EmailOptions` for configuring `email` format validation. [#903](https://github.com/Stranger6667/jsonschema/pull/903)
8+
59
### Fixed
610

711
- `multipleOf` validation for large u64 values beyond `i64::MAX` with `arbitrary-precision` feature.

crates/jsonschema-py/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- `EmailOptions` for configuring `email` format validation.
8+
59
### Fixed
610

711
- `multipleOf` validation for large integers beyond `i64::MAX`.

crates/jsonschema-py/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,48 @@ The available options:
405405

406406
This configuration is crucial when working with untrusted schemas where attackers might craft malicious regex patterns.
407407

408+
## Email Format Configuration
409+
410+
When validating email addresses using `{"format": "email"}`, you can customize the validation behavior beyond the default JSON Schema spec requirements:
411+
412+
```python
413+
import jsonschema_rs
414+
from jsonschema_rs import EmailOptions
415+
416+
# Require a top-level domain (reject "user@localhost")
417+
validator = jsonschema_rs.validator_for(
418+
{"format": "email", "type": "string"},
419+
validate_formats=True,
420+
email_options=EmailOptions(require_tld=True)
421+
)
422+
validator.is_valid("user@localhost") # False
423+
validator.is_valid("[email protected]") # True
424+
425+
# Disallow IP address literals and display names
426+
validator = jsonschema_rs.validator_for(
427+
{"format": "email", "type": "string"},
428+
validate_formats=True,
429+
email_options=EmailOptions(
430+
allow_domain_literal=False, # Reject "user@[127.0.0.1]"
431+
allow_display_text=False # Reject "Name <[email protected]>"
432+
)
433+
)
434+
435+
# Require minimum domain segments
436+
validator = jsonschema_rs.validator_for(
437+
{"format": "email", "type": "string"},
438+
validate_formats=True,
439+
email_options=EmailOptions(minimum_sub_domains=3) # e.g., [email protected]
440+
)
441+
```
442+
443+
Available options:
444+
445+
- `require_tld`: Require a top-level domain (e.g., reject "user@localhost")
446+
- `allow_domain_literal`: Allow IP address literals like "user@[127.0.0.1]" (default: True)
447+
- `allow_display_text`: Allow display names like "Name <[email protected]>" (default: True)
448+
- `minimum_sub_domains`: Minimum number of domain segments required
449+
408450
## External References
409451

410452
By default, `jsonschema-rs` resolves HTTP references and file references from the local file system. You can implement a custom retriever to handle external references. Here's an example that uses a static map of schemas:

crates/jsonschema-py/python/jsonschema_rs/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Draft201909Validator,
1212
Draft202012,
1313
Draft202012Validator,
14+
EmailOptions,
1415
Evaluation,
1516
FancyRegexOptions,
1617
RegexOptions,
@@ -121,6 +122,7 @@ def __hash__(self) -> int:
121122
"Draft202012Validator",
122123
"Validator",
123124
"Registry",
125+
"EmailOptions",
124126
"FancyRegexOptions",
125127
"RegexOptions",
126128
"meta",

crates/jsonschema-py/python/jsonschema_rs/__init__.pyi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ class RegexOptions:
6161
def __init__(self, size_limit: int | None = None, dfa_size_limit: int | None = None) -> None: ...
6262
def __repr__(self) -> str: ...
6363

64+
class EmailOptions:
65+
"""Configuration for email format validation."""
66+
67+
def __init__(
68+
self,
69+
require_tld: bool = False,
70+
allow_domain_literal: bool = True,
71+
allow_display_text: bool = True,
72+
minimum_sub_domains: int | None = None,
73+
) -> None: ...
74+
def __repr__(self) -> str: ...
75+
6476
PatternOptionsType = Union[FancyRegexOptions, RegexOptions]
6577

6678
class RetrieverProtocol(Protocol):
@@ -79,6 +91,7 @@ def is_valid(
7991
mask: str | None = None,
8092
base_uri: str | None = None,
8193
pattern_options: PatternOptionsType | None = None,
94+
email_options: EmailOptions | None = None,
8295
) -> bool:
8396
"""Check if a JSON instance is valid against a schema.
8497
@@ -99,6 +112,7 @@ def validate(
99112
mask: str | None = None,
100113
base_uri: str | None = None,
101114
pattern_options: PatternOptionsType | None = None,
115+
email_options: EmailOptions | None = None,
102116
) -> None:
103117
"""Validate a JSON instance against a schema.
104118
@@ -119,6 +133,7 @@ def iter_errors(
119133
mask: str | None = None,
120134
base_uri: str | None = None,
121135
pattern_options: PatternOptionsType | None = None,
136+
email_options: EmailOptions | None = None,
122137
) -> Iterator[ValidationError]:
123138
"""Iterate over all validation errors.
124139
@@ -137,6 +152,7 @@ def evaluate(
137152
registry: Registry | None = None,
138153
base_uri: str | None = None,
139154
pattern_options: PatternOptionsType | None = None,
155+
email_options: EmailOptions | None = None,
140156
) -> Evaluation:
141157
"""Evaluate an instance and return structured output.
142158
@@ -276,6 +292,7 @@ class Draft4Validator:
276292
mask: str | None = None,
277293
base_uri: str | None = None,
278294
pattern_options: PatternOptionsType | None = None,
295+
email_options: EmailOptions | None = None,
279296
) -> None: ...
280297
def is_valid(self, instance: Any) -> bool: ...
281298
def validate(self, instance: Any) -> None: ...
@@ -295,6 +312,7 @@ class Draft6Validator:
295312
mask: str | None = None,
296313
base_uri: str | None = None,
297314
pattern_options: PatternOptionsType | None = None,
315+
email_options: EmailOptions | None = None,
298316
) -> None: ...
299317
def is_valid(self, instance: Any) -> bool: ...
300318
def validate(self, instance: Any) -> None: ...
@@ -314,6 +332,7 @@ class Draft7Validator:
314332
mask: str | None = None,
315333
base_uri: str | None = None,
316334
pattern_options: PatternOptionsType | None = None,
335+
email_options: EmailOptions | None = None,
317336
) -> None: ...
318337
def is_valid(self, instance: Any) -> bool: ...
319338
def validate(self, instance: Any) -> None: ...
@@ -333,6 +352,7 @@ class Draft201909Validator:
333352
mask: str | None = None,
334353
base_uri: str | None = None,
335354
pattern_options: PatternOptionsType | None = None,
355+
email_options: EmailOptions | None = None,
336356
) -> None: ...
337357
def is_valid(self, instance: Any) -> bool: ...
338358
def validate(self, instance: Any) -> None: ...
@@ -352,6 +372,7 @@ class Draft202012Validator:
352372
mask: str | None = None,
353373
base_uri: str | None = None,
354374
pattern_options: PatternOptionsType | None = None,
375+
email_options: EmailOptions | None = None,
355376
) -> None: ...
356377
def is_valid(self, instance: Any) -> bool: ...
357378
def validate(self, instance: Any) -> None: ...
@@ -371,6 +392,7 @@ def validator_for(
371392
mask: str | None = None,
372393
base_uri: str | None = None,
373394
pattern_options: PatternOptionsType | None = None,
395+
email_options: EmailOptions | None = None,
374396
) -> Validator:
375397
"""Create a validator for the given schema.
376398

crates/jsonschema-py/src/email.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use pyo3::prelude::*;
2+
3+
/// EmailOptions(require_tld=False, allow_domain_literal=True, allow_display_text=True, minimum_sub_domains=None)
4+
///
5+
/// Configuration for email format validation.
6+
///
7+
/// This allows customization of email format validation behavior beyond the default
8+
/// JSON Schema spec requirements.
9+
///
10+
/// >>> from jsonschema_rs import validator_for, EmailOptions
11+
/// >>> validator = validator_for(
12+
/// ... {"format": "email", "type": "string"},
13+
/// ... validate_formats=True,
14+
/// ... email_options=EmailOptions(require_tld=True)
15+
/// ... )
16+
/// >>> validator.is_valid("user@localhost")
17+
/// False
18+
/// >>> validator.is_valid("[email protected]")
19+
/// True
20+
///
21+
/// Parameters:
22+
/// require_tld: Require a top-level domain (e.g., reject "user@localhost")
23+
/// allow_domain_literal: Allow IP address literals like "user@[127.0.0.1]"
24+
/// allow_display_text: Allow display names like "Name <[email protected]>"
25+
/// minimum_sub_domains: Minimum number of domain segments required
26+
#[pyclass(module = "jsonschema_rs")]
27+
pub(crate) struct EmailOptions {
28+
pub(crate) require_tld: bool,
29+
pub(crate) allow_domain_literal: bool,
30+
pub(crate) allow_display_text: bool,
31+
pub(crate) minimum_sub_domains: Option<usize>,
32+
}
33+
34+
#[pymethods]
35+
impl EmailOptions {
36+
#[new]
37+
#[pyo3(signature = (require_tld=false, allow_domain_literal=true, allow_display_text=true, minimum_sub_domains=None))]
38+
fn new(
39+
require_tld: bool,
40+
allow_domain_literal: bool,
41+
allow_display_text: bool,
42+
minimum_sub_domains: Option<usize>,
43+
) -> Self {
44+
Self {
45+
require_tld,
46+
allow_domain_literal,
47+
allow_display_text,
48+
minimum_sub_domains,
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)