Skip to main content

pydantic-jwt

CI PyPI Python License: MIT

JWT tokens as Pydantic models.

Declare your token as a model, and get parsing, claim validation, signature verification and encoding out of it — with the claims typed, autocompleted and checked like any other Pydantic field.

📖 Documentation

Install

pip install pydantic-jwt

Requires Python 3.10+, Pydantic 2.10+ and PyJWT 2.8+.

Basic usage

from pydantic_jwt import ConfigDict, Exp, JWTModel, after, uuid

SECRET = "keep-me-out-of-your-source"


class AccessToken(JWTModel):
    model_config = ConfigDict(
        algorithm="HS256",
        encoding_key=SECRET,
        decoding_key=SECRET,
    )

    sub: str
    exp: Exp = after(minutes=15)
    jti: str = uuid()

That single class is both ends of the flow.

Issue a token:

token = AccessToken(sub="user-42")
raw = str(token)  # 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

Read one back — the string is parsed, exp is validated, and the signature is verified against decoding_key:

token = AccessToken.from_token(raw)
token.sub  # 'user-42'

Anything wrong with the token is a normal Pydantic error, so it fits wherever Pydantic already does:

from pydantic import ValidationError

try:
    token = AccessToken.model_validate(untrusted)
except ValidationError as exc:
    {error["type"] for error in exc.errors()}
    # 'jwt_invalid_signature' — signed with the wrong key
    # 'jwt_claim_invalid'     — expired, wrong issuer, wrong audience
    # 'jwt_format'            — not a JWT at all
    # 'extra_forbidden'       — a claim the model does not declare

Claims

Exp, Nbf and Iat are annotated int types that validate themselves against the current time:

from pydantic_jwt import Exp, Iat, JWTModel, Nbf


class SessionToken(JWTModel):
    sub: str
    exp: Exp
    nbf: Nbf
    iat: Iat

IssClaim and AudClaim check a token was minted by the issuer you expect, for the service you are:

from typing import Annotated

from pydantic_jwt import AudClaim, IssClaim


class AccessToken(JWTModel):
    sub: str
    iss: Annotated[str, IssClaim("https://auth.example.com")]
    aud: Annotated[str | list[str], AudClaim("billing-api")]

Clock skew between servers is handled with leeway, in seconds:

from pydantic_jwt import ExpClaim

exp: Annotated[int, ExpClaim(leeway=30)]

Don't want a claim checked at all? Annotate it as a plain int. Need a rule of your own? Subclass Claim.

Three helpers build field defaults. They are evaluated per instance, so every token gets a fresh value:

from datetime import datetime, timezone

from pydantic_jwt import after, at, uuid


class SessionToken(JWTModel):
    sub: str
    exp: Exp = after(hours=1, minutes=30)
    nbf: Nbf = at(datetime(2030, 1, 1, tzinfo=timezone.utc))
    jti: str = uuid()

after() takes weeks, days, hours, minutes, seconds and milliseconds.

With FastAPI

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

app = FastAPI()
bearer_scheme = HTTPBearer()


def current_token(
    credentials: Annotated[HTTPAuthorizationCredentials, Depends(bearer_scheme)],
) -> AccessToken:
    try:
        return AccessToken.from_token(credentials.credentials)
    except ValueError:  # ValidationError and PydanticCustomError are both ValueErrors
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token",
            headers={"WWW-Authenticate": "Bearer"},
        ) from None


CurrentToken = Annotated[AccessToken, Depends(current_token)]


@app.get("/me")
def me(token: CurrentToken) -> dict[str, str]:
    return {"user": token.sub}

The endpoint body works with a typed object, not a dict of unknown claims. AccessToken also reports itself to OpenAPI as a string with format: jwt, so the schema stays readable.

A complete application — login, refresh-token rotation, scopes and error handling — is in the FastAPI guide.

Configuration

Everything lives in model_config, alongside the usual Pydantic settings:

Key Description
algorithm Algorithm used to sign and verify, e.g. "HS256".
encoding_key Key used by generate() and str().
decoding_key Key used to verify incoming tokens.
require_keys If False, tokens are accepted without signature verification when no key is configured. Defaults to True.

Both directions also take keys per call, which is handy for key rotation and multi-tenant deployments:

raw = token.generate(encoding_key=next_key, algorithm="HS256")
token = AccessToken.from_token(raw, decoding_key=next_key, algorithm="HS256")

Good to know

  • Building a model from a dict does not verify anything. AccessToken(sub="x") and AccessToken.model_validate({"sub": "x"}) construct a token you are about to sign; only from_token() (and validating from a token string) checks a signature. Don't accept an AccessToken straight from request data and treat it as authenticated.
  • str(token) signs. Convenient in f"Bearer {token}", a credential leak in a log line. Use repr(token) or token.model_dump(mode="json") for diagnostics.
  • Unknown claims are rejected. Models default to extra="forbid", so tokens from third-party issuers that add their own claims need model_config = ConfigDict(extra="ignore") or explicit fields.
  • The algorithm is never read from the token header. It always comes from your configuration, which is what defeats algorithm-confusion attacks.
  • require_keys=False accepts unverified tokens. It logs a warning and moves on. Useful in tests, dangerous everywhere else.

More in the security notes.

Documentation

Quickstart The five-minute tour
Token models JWTModel in full
Configuration Keys, algorithms, rotation
Claims Built-in and custom claim markers
Defaults after(), at(), uuid()
Validation and errors Error types and validation context
Working with raw tokens JWTStr
Security notes Sharp edges and scope
FastAPI A complete auth flow
API reference Generated from the source

Development

uv sync --all-groups
uv run pytest            # tests
uv run ruff check .      # lint
uv run mypy .            # types
uv run mkdocs serve      # docs at http://127.0.0.1:8000

The documentation site is built with MkDocs and served by Cloudflare as a static-asset Worker, rebuilt from this repository on every push. The Worker is configured in wrangler.jsonc; CI also runs mkdocs build --strict on every pull request.

License

MIT — see LICENSE.

Download files

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

Source Distribution

pydantic_jwt-0.2.0.tar.gz (133.8 kB view details)

Uploaded Source

Built Distribution

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

pydantic_jwt-0.2.0-py3-none-any.whl (13.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pydantic_jwt-0.2.0.tar.gz
  • Upload date:
  • Size: 133.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pydantic_jwt-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4c28c456c9063a9619a00961a0c46f84c92046dd5ec4c6ff97dd1d820c1395f3
MD5 ae995bcf6337d5cab10efd50bcfca01c
BLAKE2b-256 bf716fabdf0d9496aaa514d216e37f50a6079b8a1e66eab3e220a8d1ed1f7390

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on dmi03/pydantic-jwt

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

File details

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

File metadata

  • Download URL: pydantic_jwt-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pydantic_jwt-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b7273fe7ccafa3f3a47a7620272e5559ddd34583390d3a6b424e9b34bef34415
MD5 d57d27227817d45886760350e7dd4fa2
BLAKE2b-256 6ed8b9a37887d29107fef8861c7162a1ee3aaa6433ff342f2c2c02f8cd90c46a

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on dmi03/pydantic-jwt

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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page