Skip to main content

Zod-inspired dict validation for Python. Zero deps. One file.

Project description

zodify

Zod-inspired dict validation for Python. Zero deps. One file.

PyPI version Python versions License


Note: zodify is in alpha. The API is minimal and may change. Feedback and contributions are welcome!


Quick Start

from zodify import validate

config = validate(
    {"port": int, "debug": bool},
    {"port": 8080, "debug": True},
)

That's it. Plain dicts in, validated dicts out. No classes, no DSL, no dependencies.


Why zodify?

Most validation libraries ask you to learn a new DSL or model system. zodify doesn't.

zodify zon zodic Pydantic
Philosophy Minimalist Full Zod port Full Zod port Full ORM
API style Plain dicts Chained builders Chained builders Classes
Dependencies 0 2 0 Many
Code size ~250 LOC 1,000s+ LOC 1,000s+ LOC Large
Learning curve Zero Must learn DSL Must learn DSL Must learn DSL
Env var support Built-in No No Partial

Install

pip install zodify

Requires Python 3.10+

To install from source:

git clone https://github.com/junyoung2015/zodify.git && pip install ./zodify

Usage

validate() — Dict Schema Validation

Define a schema as a plain dict of key: type pairs, then validate any dict against it.

from zodify import validate

schema = {"port": int, "debug": bool, "name": str}

# Exact type match
result = validate(schema, {"port": 8080, "debug": True, "name": "myapp"})
# → {"port": 8080, "debug": True, "name": "myapp"}

# Coerce strings — great for env vars and config files
raw = {"port": "8080", "debug": "true", "name": "myapp"}
result = validate(schema, raw, coerce=True)
# → {"port": 8080, "debug": True, "name": "myapp"}

# All errors are collected at once
validate({"a": int, "b": str}, {"a": "x", "b": 42})
# ValueError: a: expected int, got str
#             b: expected str, got int

Parameters:

Param Type Default Description
schema dict Mapping of keys to expected types (str, int, ...)
data dict The dict to validate
coerce bool False Cast string values to the target type when possible
max_depth int 32 Maximum nesting depth to prevent stack overflow
unknown_keys str "reject" How to handle extra keys: "reject" or "strip"

Behavior:

  • Extra keys in data are rejected by default (unknown_keys="reject").
  • Use unknown_keys="strip" to silently drop extra keys and return only schema-declared keys.
  • Missing keys raise ValueError.
  • When coerce=True, only str inputs are coerced (non-string mismatches still error).
  • Bool coercion accepts: true/false, 1/0, yes/no (case-insensitive).
# Default: reject unknown keys
validate({"name": str}, {"name": "kai", "age": 25})
# ValueError: age: unknown key

# Opt-in: strip unknown keys
validate(
    {"name": str},
    {"name": "kai", "age": 25},
    unknown_keys="strip",
)
# -> {"name": "kai"}

Nested Dict Validation

Your schema can contain nested dicts — validation recurses automatically.

schema = {"db": {"host": str, "port": int}}

validate(schema, {"db": {"host": "localhost", "port": 5432}})
# → {"db": {"host": "localhost", "port": 5432}}

validate(schema, {"db": {"host": "localhost", "port": "bad"}})
# ValueError: db.port: expected int, got str

Errors use dot-notation paths: db.host, a.b.c, etc.


Optional Keys

Use Optional to mark keys that can be missing. Provide a default, or omit it to exclude the key from results.

from zodify import validate, Optional

schema = {
    "host": str,
    "port": Optional(int, 8080),     # default 8080
    "debug": Optional(bool),          # absent if missing
}

validate(schema, {"host": "localhost"})
# → {"host": "localhost", "port": 8080}

Note: Optional shadows typing.Optional. If you use both in the same file, alias it: from zodify import Optional as Opt or use zodify.Optional(...).


List Element Validation

Use a single-element list as the schema value to validate every element in the list.

validate({"tags": [str]}, {"tags": ["python", "config"]})
# → {"tags": ["python", "config"]}

validate({"tags": [str]}, {"tags": ["ok", 42]})
# ValueError: tags[1]: expected str, got int

List of dicts works too:

validate(
    {"users": [{"name": str, "age": int}]},
    {"users": [{"name": "Alice", "age": 30}]},
)

Combined Example

All features compose naturally:

from zodify import validate, Optional

schema = {
    "db": {"host": str, "port": Optional(int, 5432)},
    "tags": [str],
    "debug": Optional(bool, False),
}

validate(schema, {
    "db": {"host": "localhost"},
    "tags": ["prod"],
})
# → {"db": {"host": "localhost", "port": 5432},
#    "tags": ["prod"], "debug": False}

env() — Typed Environment Variables

Read and type-cast environment variables with a single call.

from zodify import env

port   = env("PORT", int, default=3000)
debug  = env("DEBUG", bool, default=False)
secret = env("SECRET_KEY", str)  # raises ValueError if missing

Parameters:

Param Type Default Description
name str Environment variable name
cast type Target type (str, int, float, bool)
default any (none) Fallback if the var is unset. Not type-checked.

Release Process

Release automation is tag-driven:

  • Pushing a tag that matches v* triggers .github/workflows/publish.yml.
  • The workflow runs tests, builds distributions, publishes to PyPI, and creates a GitHub Release.
  • GitHub Release notes are sourced from the matching section in CHANGELOG.md (for example, ## [v0.1.0]).

Run local preflight before tagging:

./scripts/release_preflight.sh

If preflight passes, push the version tag:

git tag v0.1.0
git push origin v0.1.0

Roadmap

zodify is in alpha (v0.1.0). The API surface is small and may evolve.

Shipped in v0.1.0:

  • Nested schema validation
  • Optional keys with defaults
  • List element validation

Near-term:

  • Custom validator functions
  • Chained builder API

Exploring:

  • .env file loading
  • JSON Schema export
  • Framework integrations (FastAPI, Django)
  • Async validation support

License

MIT — 2026 Jun Young Sohn

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zodify-0.2.0.tar.gz (18.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zodify-0.2.0-py3-none-any.whl (7.6 kB view details)

Uploaded Python 3

File details

Details for the file zodify-0.2.0.tar.gz.

File metadata

  • Download URL: zodify-0.2.0.tar.gz
  • Upload date:
  • Size: 18.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for zodify-0.2.0.tar.gz
Algorithm Hash digest
SHA256 72cd954f1491629a95b511049c768a2e88353d76d4fe18ba31a194b31431b2d3
MD5 bd569c09d399cc12dc6118dddf6cb48c
BLAKE2b-256 9ff2e6c71fda8096a9f88a64442e540af02ec2b5000e550b4099aac9bf67b172

See more details on using hashes here.

Provenance

The following attestation bundles were made for zodify-0.2.0.tar.gz:

Publisher: publish.yml on junyoung2015/zodify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zodify-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: zodify-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 7.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for zodify-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bbbfc84d151cd6ab402aac9d88ddb65fc379bdad04e09d31d92bb357fef573d9
MD5 c1d13873dc8e0f2c275350c302147483
BLAKE2b-256 7bc059bd206796bc3919e410924f1a54e269692a970494f395ce05579e6f7203

See more details on using hashes here.

Provenance

The following attestation bundles were made for zodify-0.2.0-py3-none-any.whl:

Publisher: publish.yml on junyoung2015/zodify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page