Metadata-Version: 2.4
Name: super-easy-validator-python
Version: 0.1.0
Summary: Validate data with rules you write as plain strings, like 'optional|email'. Zero dependencies, no schemas.
Author-email: Rituraj Shakti <riturajshakti@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/riturajshakti/super-easy-validator-python
Project-URL: Documentation, https://github.com/riturajshakti/super-easy-validator-python/blob/main/DOCS.md
Project-URL: Changelog, https://github.com/riturajshakti/super-easy-validator-python/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/riturajshakti/super-easy-validator-python/issues
Keywords: validator,validation,validate,schema,schema-validation,data-validation,input-validation,form-validation,request-validation,validation-rules,zero-dependency,json-validation,dict-validation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# super-easy-validator-python

[![PyPI](https://img.shields.io/pypi/v/super-easy-validator-python)](https://pypi.org/project/super-easy-validator-python/)
[![Python](https://img.shields.io/pypi/pyversions/super-easy-validator-python)](https://pypi.org/project/super-easy-validator-python/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](pyproject.toml)

**Validate data with rules you write as plain strings.** Zero dependencies, fully typed. No schemas, no model classes — just `"optional|email"`.

Also available for JavaScript and TypeScript: [super-easy-validator](https://www.npmjs.com/package/super-easy-validator) on npm, [@riturajshakti/super-easy-validator](https://jsr.io/@riturajshakti/super-easy-validator) on JSR. And for Go: [super-easy-validator-go](https://pkg.go.dev/github.com/riturajshakti/super-easy-validator-go).

```sh
pip install super-easy-validator-python
```

**[📖 Full documentation — guide and complete API reference](DOCS.md)**

## Why

```python
# super-easy-validator-python
{"age": "optional|natural|min:18"}

# the usual alternative
class User(BaseModel):
    age: int | None = Field(default=None, gt=0, ge=18)
```

- **Zero runtime dependencies**
- Rules are data, so they can be built at runtime, loaded from config, or shared
- Validates decoded JSON directly — no model class required
- Nested objects, arrays of objects, per-element array rules, custom messages

## Quick start

```python
from super_easy_validator_python import validate

rules = {
    "name": "fullname",
    "email": "email",
    "password": "string|min:8",
    "age": "optional|natural|min:18",
    "role": "enums:admin,user,guest",
    "website": "optional|url",
}

data = {
    "name": "John",
    "email": "not-an-email",
    "password": "abc",
    "age": 15,
    "role": "superuser",
    "website": "example.com",
}

result = validate(rules, data)
if result.errors:
    for message in result.errors:
        print(message)
```

```
name must be a valid fullname
email must be a valid email
password must have length of at least 8
age must be at least 18
role is invalid
website must be a valid url
```

`result.errors` is `None` when everything passes, so `if result.errors:` is the idiom. `result.valid` says the same thing the other way round.

Alongside `errors`, the `details` list pairs each message with a stable code and the field it came from:

```python
result = validate({"age": "natural|min:18"}, {"age": 15})
result.details[0]
# {'field': 'age', 'message': 'age must be at least 18', 'code': 'TOO_SMALL'}
```

Details are plain dicts, so they serialize straight to JSON — useful when returning validation errors from an API.

Three other ways to read a result:

```python
errors, details = validate(rules, data)      # tuple unpacking
if not validate(rules, data): ...            # falsy when invalid
validate(rules, data).raise_for_errors()     # raises ValidationError
```

## Data, and the three states

Data is a plain `dict` — what `json.loads` gives you:

| State | Written as | Satisfies |
|---|---|---|
| absent | key not in the dict | `optional` |
| null | key present, value `None` | `nullable` |
| present | key present, value set | the rest of the rules |

Dataclasses and model classes are not accepted: every field always exists on them, so `optional` and `nullable` would collapse into one. Convert first with `dataclasses.asdict(obj)`.

## Structure: nested objects, arrays, and indexing

```python
rules = {
    "address": {
        "city": "name",
        "pin": "string|natural|size:6",
        "country": {"code": "alpha|upper|size:2"},
    },
    "tags": "array|min:2|arrayof:string|arrayof:max:10",
    "users": [{"name": "name", "age": "natural"}],
    "grid": [[{"label": "string"}]],          # arrays of arrays of objects

    "coords": "array|size:2",
    "coords[0]": "number|min:-90|max:90",     # latitude
    "coords[1]": "number|min:-180|max:180",   # longitude
    "history[-1]": "date",                    # the most recent entry
    "matrix": "arrayof:arrayof:number",       # arrays of arrays
}
```

Errors carry the full path, including array indexes:

```
address.pin must be a valid numeric string
address.country.code must not contains lower case letters
tags[1] must have length of at most 10
users[1].name is required
coords[0] must be at most 90
history[-1] must be a valid date
matrix[1][0] must be a valid number
```

Slices work too — `"c[0:2]"`, `"c[1:]"`, `"c[-2:]"` — applying the rule to each selected element.

## Operators: `$or`, `$and`, `$switch`

`$or` passes if any branch passes. `$and` requires every branch. `$switch` applies one rule, chosen by which `case` matches. Branches accept any rule value: strings, nested rules, list rules, functions, or nested operators.

Operators are dict keys:

```python
rules = {
    # one field, several valid shapes
    "address": {"$or": [
        "string|max:60",
        {"city": "name", "pin": "string|natural|size:6"},
    ]},

    # $and with optional makes a nested object or list optional
    "billing": {"$and": [
        "optional",
        {"line1": "string|min:5", "city": "name"},
    ]},

    # one rule chosen by case; default supplies the error when nothing matches
    "amount": {"$switch": [
        {"case": "number|max:1000", "then": "positive", "default": True},
        {"case": "number|min:1001", "then": "positive|decimalmax:2"},
    ]},
}
```

`$or` reports the branch that best fits the value, so you get `address.pin ...` rather than a vague "address is invalid". `$and` merges its object branches, so a key declared in any branch counts as declared under `strict`.

Because operators are plain dict keys, a whole rule tree can be loaded from JSON:

```python
rules = json.load(open("rules.json"))
result = validate(rules, data)
```

## Custom rules and cross-field validation

A rule value can be a function. It receives the value and its parent, and returns `None` to pass or a dict to fail — so you own the wording and the code, and cross-field checks need no special syntax.

```python
rules = {
    "password": "string|min:8",
    "confirm_password": lambda value, parent: (
        None if value == parent["password"]
        else {"message": "passwords must match", "code": "PASSWORD_MISMATCH"}
    ),
}
```

Combine a function with built-in rules through `$and`:

```python
{"n": {"$and": ["natural", is_even]}}
```

The function is called even when the value is absent, so it owns the decision about absence. An exception inside a custom rule is reported as a rule error naming the field, rather than crashing the caller.

## Every rule at a glance

Combine rules with `|`, or pass a list — `["string", "min:3"]` — when a rule contains a `|` itself.

| Group | Rules |
|---|---|
| **Presence** | `optional` `nullable` `$atleast` `$atmost` |
| **Types** | `string` `number` `boolean` `array` `object` |
| **Strings** | `email` `url` `domain` `name` `fullname` `username` `alpha` `alphanumeric` `phone` `phonecode` `objectid` `uuid` `date` `dateonly` `time` `lower` `upper` `ip` |
| **Numbers** | `int` `positive` `negative` `natural` `whole` |
| **Constraints** | `equal:` `size:` `min:` `max:` `regex:` `decimalsize:` `decimalmin:` `decimalmax:` `enums:` |
| **Arrays** | `arrayof:<any rule above>` |
| **Operators** | `$or` `$and` `$switch` |
| **Messages** | `field:` `error:` and a `quotes` option |

String rules check for a string automatically; number rules check for a number. Prefix with `string` to validate numeric or boolean strings — `"string|natural"`, `"string|boolean"`.

An unknown rule raises `InvalidRuleError` rather than being ignored, so a typo surfaces at first run.

## Numbers

`bool` is a subclass of `int` in Python, so `True` would otherwise satisfy every numeric rule. It does not here: numeric rules reject booleans, and `boolean` rejects numbers.

Python integers are arbitrary precision, so large values keep their digits with no special rule:

```python
validate({"f": "natural"}, json.loads('{"f": 12345678901234567890}'))  # exact
```

## Regular expressions

Patterns may be written in literal form and are translated automatically:

```python
{"hash": r"regex:/^[A-Z0-9]{128}$/i"}   # or plain: r"regex:(?i)^[A-Z0-9]{128}$"
```

Flags `i`, `m` and `s` are honoured; `g`, `y` and `u` are accepted and ignored. Lookahead, lookbehind and backreferences all work.

## Options

```python
from super_easy_validator_python import Config

validate(rules, data, Config(quotes="backtick", strict=True))
```

- **`quotes`** — `"none"` (default), `"single-quotes"`, `"double-quotes"`, `"backtick"`
- **`strict`** — reject any field in the data that has no rule, nested objects included
- **`array_indexing_check`** — `True` by default. Set `False` to treat keys like `"c[0]"` as literal names

## Typing

The package ships `py.typed`, so mypy and pyright see the annotations. `Rules` is `dict[str, Any]`, which is honest about how dynamic a rule tree is.

## License

MIT
