pydantic-jwt
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.
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")andAccessToken.model_validate({"sub": "x"})construct a token you are about to sign; onlyfrom_token()(and validating from a token string) checks a signature. Don't accept anAccessTokenstraight from request data and treat it as authenticated. str(token)signs. Convenient inf"Bearer {token}", a credential leak in a log line. Userepr(token)ortoken.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 needmodel_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=Falseaccepts 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4c28c456c9063a9619a00961a0c46f84c92046dd5ec4c6ff97dd1d820c1395f3
|
|
| MD5 |
ae995bcf6337d5cab10efd50bcfca01c
|
|
| BLAKE2b-256 |
bf716fabdf0d9496aaa514d216e37f50a6079b8a1e66eab3e220a8d1ed1f7390
|
Provenance
The following attestation bundles were made for pydantic_jwt-0.2.0.tar.gz:
Publisher:
publish.yml on dmi03/pydantic-jwt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_jwt-0.2.0.tar.gz -
Subject digest:
4c28c456c9063a9619a00961a0c46f84c92046dd5ec4c6ff97dd1d820c1395f3 - Sigstore transparency entry: 2639232644
- Sigstore integration time:
-
Permalink:
dmi03/pydantic-jwt@e37a65161ffc07c10141417d2286e8b41c792950 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dmi03
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e37a65161ffc07c10141417d2286e8b41c792950 -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7273fe7ccafa3f3a47a7620272e5559ddd34583390d3a6b424e9b34bef34415
|
|
| MD5 |
d57d27227817d45886760350e7dd4fa2
|
|
| BLAKE2b-256 |
6ed8b9a37887d29107fef8861c7162a1ee3aaa6433ff342f2c2c02f8cd90c46a
|
Provenance
The following attestation bundles were made for pydantic_jwt-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on dmi03/pydantic-jwt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_jwt-0.2.0-py3-none-any.whl -
Subject digest:
b7273fe7ccafa3f3a47a7620272e5559ddd34583390d3a6b424e9b34bef34415 - Sigstore transparency entry: 2639232696
- Sigstore integration time:
-
Permalink:
dmi03/pydantic-jwt@e37a65161ffc07c10141417d2286e8b41c792950 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/dmi03
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e37a65161ffc07c10141417d2286e8b41c792950 -
Trigger Event:
workflow_dispatch
-
Statement type: