User Guide
Encoding a JWT ⏫¶
The function encode() produces a compact (three-part) signed JWT/JWS.
See encode() parameters
| Function Parameters | Type | Description |
|---|---|---|
claims |
JWTClaims | dict | None |
The claims data for the JWT payload. |
key |
Key | bytes | str |
The key to sign the JWT with (a secret key, a private key in PEM format, or a Key instance). |
algorithm |
Alg | str |
The signing algorithm (e.g., Alg.HS256 or "HS256"). |
headers |
JOSEHeader | dict | None |
(optional) Custom JOSE headers. |
detach_payload |
bool |
(optional) If True, produces a detached payload JWT. |
validation |
type[JWTBaseModel]| ValidationConfig| Validation |
(optional) Validation settings for claims. If claims is a Pydantic model it is validated automatically. |
headers_validation |
type[JWTBaseModel]| ValidationConfig| Validation |
(optional) Validation settings for headers. If headers is a Pydantic model it is validated automatically. |
Examples¶
The JWTClaims Pydantic model allows you to create and validate all official registered claims automatically. See JWTClaims Pydantic model.
from superjwt import Alg, JWTClaims, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims = JWTClaims(iss="my-app", sub="John Doe")
print(claims)
#> JWTClaims(iss='my-app', sub='John Doe', aud=None, iat=None, nbf=None, exp=None, jti=None)
compact: bytes = encode(claims, secret_key, Alg.HS256)
print(compact)
#> b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
# .eyJpc3MiOiJteS1hcHAiLCJzdWIiOiJKb2huIERvZSJ9
# .HwnUqTLFAMzNkMrokd0aI7c-zSJJpSVXMrYIhUyWe4s'
print(inspect(compact).payload)
#> {'iss': 'my-app', 'sub': 'John Doe'}
You can define your claims manually from a Python dict.
from superjwt import Alg, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims_dict = {"iss": "my-app", "sub": "John Doe"}
compact = encode(claims_dict, secret_key, Alg.HS256) # (1)
print(inspect(compact).payload)
#> {'iss': 'my-app', 'sub': 'John Doe'}
- When encoding from a raw
dict, the claims are automatically validated againstJWTClaimsto ensure compliance with JWT standards. You can also disable validation.
from superjwt import Alg, JWTClaims, encode
from superjwt.exceptions import ClaimsValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
claims = JWTClaims.model_construct(jti=1234) # (1)
try:
encode(claims, secret_key, Alg.HS256) # --> ❌ fails (2)
except ClaimsValidationError as e:
print(e)
#> ClaimsValidationError: Claims validation failed
#> claim ('jti',) = 1234 -> validation failed (string_type): Input should be a valid string
.model_construct()creates a Pydantic instance without validating its model. This allows for the creation of an invalid Pydantic instance without raising apydantic.ValidationError.- During encoding, if the
claimsobject is a Pydantic instance, validation runs automatically based on its own Pydantic model. Since'jti'is not astr, aClaimsValidationErroris raised. To disable validation, see Disable Validation.
You can add custom claims as extra fields beyond the registered claims. However, these fields won't be validated unless you define your own Pydantic model. See the custom model example.
from superjwt import Alg, JWTClaims, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims = JWTClaims(
sub="Alice", jti="jwt-id",
custom_claim="a string",
custom_date=1766536919 # (1)
)
compact = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'sub': 'Alice', 'jti': 'jwt-id', 'custom_claim': 'a string', 'custom_date': 1766536919}
- Without a custom Pydantic model, you cannot pass a Python
datetimeobject and have it is automatically serialized as a UNIX timestamp.
Extra claims
The JWTClaims Pydantic model is configured with extra="allow", which allows adding custom claims without explicit definition. These custom claims will not have validation rules during encode() or decode(). To include validation, use a custom model that inherits from JWTClaims. See Pydantic Models and Validation.
See Custom Models for more information about custom Pydantic models and custom validation.
from datetime import datetime
from pydantic import AfterValidator, Field
from superjwt import Alg, JWTClaims, JWTDatetimeInt, encode, inspect
from typing import Annotated
from uuid import UUID
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
sub: int = Field(default=...) # 'sub' is redefined as a required integer
user_id: Annotated[str, AfterValidator(lambda x: str(UUID(x, version=4)))] # must be UUIDv4
custom_date: JWTDatetimeInt # must be a datetime/timestamp
claims = MyJWTClaims(
sub=1234,
user_id="d134196e-f27e-4c0b-a7b8-fedca264e51f",
custom_date=datetime(2025, 12, 31, 23, 59, 59, 987654) # (1)
)
compact: bytes = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'sub': 1234, 'user_id': 'd134196e-f27e-4c0b-a7b8-fedca264e51f', 'custom_date': 1767225599}
- We passed a Python
datetimeto theJWTDatetimefield'custom_date'. It is automatically serialized as a timestamp in the JSON output. See Datetime Fields.
Add Issued At ⇨ 'iat'¶
You can automatically add the 'iat' (Issued At) claim. The value will be set to the current UTC time.
from superjwt import Alg, JWTClaims, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims = (
JWTClaims(iss="my-app", sub="John Doe")
.with_issued_at()
)
print(claims)
#> JWTClaims(iss='my-app', sub='John Doe', aud=None,
# iat=datetime.datetime(2026, 1, 8, 2, 9, 15, 603171, tzinfo=datetime.timezone.utc), (1)
# nbf=None, exp=None, jti=None)
compact: bytes = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'iss': 'my-app', 'sub': 'John Doe', 'iat': 1767838155} (2)
- We have added the Issued At claim. In the
JWTClaimsPydantic instance, the value is stored as a Pythondatetime(UTC). It is automatically serialized as a timestamp in the JSON output. - By default,
'iat'timestamp is serialized to an integer, but this can be changed to a float. See Datetime Fields.
Set Expiration ⇨ 'exp'¶
Use .with_expiration() to return a new JWTClaims instance with the 'exp' timestamp set. Choose your desired expiration as a duration from the time of creation. It accepts days, hours, and minutes as arguments.
from superjwt import Alg, JWTClaims, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims = (
JWTClaims(sub="Jane Doe")
.with_expiration(minutes=15)
)
compact = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'sub': 'Jane Doe', 'exp': 1767045509} (1)
- By default,
'exp'timestamp is serialized to an integer, but this can be changed to a float. See Datetime Fields.
Use .with_expiration() chained with .with_issued_at() to return a new JWTClaims instance with both updated 'iat' and 'exp' timestamp claims.
from superjwt import Alg, JWTClaims, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims = (
JWTClaims(sub="Jane Doe")
.with_issued_at()
.with_expiration(minutes=10)
)
compact = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'sub': 'Jane Doe', 'iat': 1767044818, 'exp': 1767045418}
You can use a Python dict and add the 'exp' timestamp manually.
# Python 3.11+
from datetime import datetime, timedelta, UTC
from superjwt import Alg, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
claims = {"sub": "Jane Doe", "exp": (datetime.now(UTC) + timedelta(minutes=15)).timestamp()}
compact = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'sub': 'Jane Doe', 'exp': 1767046329} # (1)
- Even though we manually passed the
'exp'claim as a float, the claims is validated againstJWTClaims. Therefore,'exp'is then serialized automatically to an integer, but this can be changed to a float. See Datetime Fields.
Date/time objects
iat, exp and nbf represent all date/time information and are serialized as UNIX timestamps in the payload. But in a Pydantic model, they are stored as Python datetime objects! See more in Datetime Fields.
Decoding a JWT ⏬¶
The function decode() decodes and verifies a compact (three-part) signed JWT/JWS. It also performs validation on JWT content unless disabled. It returns the claims data as a Pydantic instance from the model that ran the validation (default is JWTClaims).
See decode() parameters
| Function Parameters | Type | Description |
|---|---|---|
compact |
bytes | str |
The JWT compact token to decode. |
key |
Key | bytes | str |
The key to verify the JWT with (a secret key, a private key in PEM format, or a Key instance). |
algorithm |
Alg | str |
The verifying algorithm (e.g., Alg.HS256 or "HS256"). |
with_detached_payload |
JWTClaims | dict | None |
(optional) The detached payload data, if the token was encoded with a detached payload. |
validation |
type[JWTBaseModel]| ValidationConfig| Validation |
(optional) Validation settings for claims. See Validation. |
headers_validation |
type[JWTBaseModel]| ValidationConfig| Validation |
(optional) Validation settings for headers. By default, headers are validated against JOSEHeader. |
JWT Signature Verification
When using the decode() function, the JWT compact token is automatically verified. If the content of the JWT has been tampered with, or if the key used for decoding is incorrect, verification fails and a SignatureVerificationError is raised. See the example below.
Examples¶
During decoding, compact token are automatically verified and validated against JWTClaims Pydantic model.
from superjwt import Alg, JWTClaims, decode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
compact = (
b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
b"eyJpc3MiOiJteS1hcHAiLCJzdWIiOiJKb2huIERvZSJ9."
b"HwnUqTLFAMzNkMrokd0aI7c-zSJJpSVXMrYIhUyWe4s"
)
print(inspect(compact).payload) # (1)
#> {'iss': 'my-app', 'sub': 'John Doe'}
decoded: JWTClaims = decode(compact, secret_key, Alg.HS256) # (2)
print(decoded)
#> iss='my-app' sub='John Doe' aud=None iat=None nbf=None exp=None jti=None
print(decoded.to_dict())
#> {'iss': 'my-app', 'sub': 'John Doe'}
print(decoded.sub)
#> 'John Doe'
-
Unverified JWT
Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT. Only the
decode()function guarantees integrity. -
The token was successfully verified and validated against the
JWTClaimsPydantic model. Use thevalidationparameter to validate against your own custom models. See Validation.
The token may have been tampered with!
from superjwt import Alg, decode, inspect
from superjwt.exceptions import SignatureVerificationError
secret_key = "your-secret-key-of-len-32-bytes!"
compact = (
b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
b"eyJjYW5fSV90cnVzdF95b3UiOiJubyJ9."
b"BsUynvYTk4w4_TCS39qAUoovSmS7hJxG4fahZGK9RrY"
)
try:
decode(compact, secret_key, Alg.HS256) # --> ❌ fails (1)
except SignatureVerificationError as e:
print(inspect(compact).payload)
#> {'can_I_trust_you': 'no'}
print(e)
#> Signature verification failed, the token may have been tampered with!
- 😱 The token might have been tampered with, or the secret key is incorrect.
When claims validation fails, a ClaimsValidationError is raised. This does not change the fact that the token has been verified and is authentic. See Validation.
from superjwt import Alg, JWTBaseModel, Validation, decode, inspect
from superjwt.exceptions import ClaimsValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
compact = (
b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
b"eyJpc3MiOnRydWV9."
b"PfIcUJHW8m8qRD-Lu4Sj5tCuN1cRGjNAjhxtXzXM6_U"
)
print(inspect(compact).payload) # (1)
#> {'iss': True}
try:
decode(compact, secret_key, Alg.HS256) # --> ❌ fails (2)
except ClaimsValidationError as e:
print(e)
#> Claims validation failed
#> claim ('iss',) = True -> validation failed (string_type): Input should be a valid string
decoded: JWTBaseModel = decode(
compact, secret_key, Alg.HS256, validation=Validation.DISABLE
) # --> 🤔 passes (3)
print(decoded.to_dict())
#> {'iss': True}
-
Unverified JWT
Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.
-
By default,
decode()validates claims againstJWTClaims. Since'iss'must be a string, validation fails. - To decode without validation, explicitly use
Validation.DISABLE. The JWT is still verified, proving its authenticity. The returning Pydantic instance is aJWTBaseModel, a base model with no validation or defined fields. See Pydantic Models.
See Custom Models.
from pydantic import Field
from superjwt import Alg, JWTClaims, decode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
custom_field: int = Field(default=...)
compact = (
b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
b"eyJzdWIiOiJ1c2VyIiwiY3VzdG9tX2ZpZWxkIjo0Mn0."
b"m4CHuuAgVICiDVeDcJwTT7Vf0yG3skwzsyp9mroxdw0"
)
print(inspect(compact).payload) # (1)
#> {'sub': 'user', 'custom_field': 42}
decoded: MyJWTClaims = decode(compact, secret_key, Alg.HS256, validation=MyJWTClaims)
print(decoded.to_dict())
#> {'sub': 'user', 'custom_field': 42}
-
Unverified JWT
A compact token inspection DOES NOT verify the signature! Never trust the information from an unverified JWT. Only the
decode()function will prove the JWT integrity.
See Validation Config.
from pydantic import Field
from superjwt import Alg, JWTClaims, ValidationConfig, decode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
custom_field: int = Field(default=...)
validation = ValidationConfig(
model=MyJWTClaims,
leeway=30.0, # (1)
)
compact = (
b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
b"eyJzdWIiOiJ1c2VyIiwiaWF0IjoxNzY3ODQ1OTM4LCJleHAiOjIxOTk3NTY0NzUsImN1c3RvbV9maWVsZCI6NDJ9."
b"U8qku24iP0PVTfqmU_PqaCTVu62Kz-gf6h9jjVwRt_k"
)
print(inspect(compact).payload) # (2)
#> {'sub': 'user', 'iat': 1767845938, 'exp': 2199756475, 'custom_field': 42}
decoded: MyJWTClaims = decode(compact, secret_key, Alg.HS256, validation=validation)
print(decoded) # (3)
#> iss=None sub='user' aud=None iat=datetime.datetime(2026, 1, 8, 4, 18, 58, tzinfo=TzInfo(0)) nbf=None
# exp=datetime.datetime(2039, 9, 16, 3, 27, 55, tzinfo=TzInfo(0)) jti=None custom_field=42
print(decoded.to_dict())
#> {'sub': 'user', 'iat': 1767845938, 'exp': 2199756475, 'custom_field': 42}
- By creating a
ValidationConfiginstance to be passed tovalidationparameter, you can change other behaviors like the leeway when decoding'iat','exp','nbf'claims. -
Unverified JWT
A compact token inspection DOES NOT verify the signature! Never trust the information from an unverified JWT. Only the
decode()function will prove the JWT integrity. -
The
'iat'and'exp'claims are stored as a Pythondatetimeobject. See Datetime Fields.
Token Expired¶
A token that has expired (i.e., its 'exp' value is in the past) will raise a TokenExpiredError during validation.
Code Example
# Python 3.11+
from datetime import datetime, timedelta, UTC
from superjwt import Alg, JWTClaims, Validation, decode, encode
from superjwt.exceptions import TokenExpiredError
secret_key = "your-secret-key-of-len-32-bytes!"
# create a fake expired compact token
compact = encode(
JWTClaims.model_construct(exp=datetime.now(UTC) - timedelta(days=1)), # (1),
secret_key,
Alg.HS256,
validation=Validation.DISABLE
)
# decode token
try:
decode(compact, secret_key, Alg.HS256) # --> ❌ fails (2)
#> TokenExpiredError: Token has expired
except TokenExpiredError:
decoded = decode(
compact, secret_key, Alg.HS256, validation=Validation.DISABLE
) # --> 🤔 passes (3)
print(decoded.exp)
#> 1766960212
.model_construct()creates a Pydantic instance without running validation.- Since
'exp'is in the past, the token failsJWTClaimsvalidation. - If we decode without claims validation, the token is returned (it was successfully verified), regardless of its content.
OAuth2.0 and short-lived JWT token
In a production environment using OAuth2.0, a logged-in user (the client) typically receives a short-lived "Access Token" (e.g., 15 minutes). Instead of requiring a login every 15 minutes, the server can issue a new Access Token for the user. See the OAuth2.0 Basic Example for real-world scenarios.
Inspecting Tokens¶
For debugging purposes, you can inspect a token without verifying its signature:
from superjwt import JWSToken, inspect
compact = (
b"eyJhbGciOiJOb05lIiwidHlwIjoiSldUIn0"
b"."
b"eyJjYW5fSV90cnVzdF95b3UiOiJubyJ9"
b"."
b"BsUynvYTk4w4_TCS39qAUoovSmS7hJxG4fahZGK9RrY"
)
token: JWSToken = inspect(compact)
print(token.payload)
#> {'can_I_trust_you': 'no'}
print(token.headers)
#> {'alg': 'NoNe', 'typ': 'JWT'}
Unsafe operation
The inspect() function does NOT verify the JWT content. This means the token could be tampered with or forged.
NEVER rely on inspect() in production or for security-critical operations. Use it only for debugging and development.
Pydantic Models ♦️¶
SuperJWT uses Pydantic for automatic validation and serialization of JWT claims and headers. You can use ready-made Pydantic models or create your own by inheriting from the following base models.
JWTBaseModel¶
The base Pydantic model for SuperJWT, which inherits from pydantic.BaseModel. All Pydantic models used in this package derive from JWTBaseModel. It includes the following features:
- Extra Fields
By default, extra fields are allowed, even if not explicitly defined. - Serialization
The.to_dict()method serializes non-empty fields into a Pythondict. This is similar topydantic.BaseModel.model_dump(exclude_none=True)but with internal context injected. - Internal Settings
>now: Allows for time spoofing by setting a value for the "present time".
>jwtdatetime_force_int: Forces allJWTDatetimetimestamps to be integers instead of floats (default:True).
See Validation Config. - Auto-Revalidation
Automatically revalidates Pydantic instances (equivalent torevalidate_instances="always").
JOSEHeader¶
Inherits from JWTBaseModel and defines a compliant set of protected headers (JOSE Header).
Properties:
- Protected Header Values
Defines a mandatoryalgfield and other optional fields such as'typ'='JWT','kid', and'crit'. - Default Headers Model for Validation
Headers are validated against this model whenheaders_validationis not set indecode(). -
Make Default Method
Creates header data with the required'alg'field populated. -
No Support for
b64=false
JWTClaimsModel
Inherits from JWTBaseModel: an internal model that defines all standard JWT registered claims.
JWTClaims¶
Inherits from JWTClaimsModel and defines a compliant JWT claims set.
Properties:
-
Compliance with RFC 7519
A compliant Pydantic model for standard JWT payloads. Defines all standard registered claims with proper Python types.List of registered claims
'iss', optionalstr'sub', optionalstr'aud', optionalstrorlist[str]'iat', optionalJWTDatetime'nbf', optionalJWTDatetime'exp', optionalJWTDatetime'jti', optionalstr
Code Examples
from pydantic import ValidationError from superjwt import JWTClaims try: JWTClaims(sub=1234, user_id=1234) #> ValidationError: 1 validation error for JWTClaims #> sub #> Input should be a valid string [type=string_type, input_value=1234, input_type=int] except ValidationError: claims = JWTClaims(sub="1234", user_id=1234) print(claims.to_dict()) #> {'sub': '1234', 'user_id': 1234}from pydantic import ValidationError from superjwt import JWTClaims claims = {"iss": 123, "sub": "user_123", "custom_claim": "hello"} try: JWTClaims(**claims) #> ValidationError: 1 validation error for JWTClaims #> iss #> Input should be a valid string [type=string_type, input_value=123, input_type=int] except ValidationError: claims = JWTClaims.model_construct(**claims).to_dict() # do not validate model print(claims.to_dict()) #> {'iss': 123, 'sub': 'user_123', 'custom_claim': 'hello'} -
Default Claims Model for Validation
Claims are validated against this model whenvalidationparameter is not set indecode(). - Time Integrity Checks
Ensures the following conditions are met for'iat','nbf', and'exp':'iat'< now (can be disabled withallow_future_iat, see Validation Config)'nbf'< now'exp'> now
- Token Time Validity
RaisesTokenExpiredErrorif the token has expired (given'exp'claim timestamp) orTokenNotYetValidErrorif it is not yet valid (given'nbf'claim timestamp during decoding only). - Time Leeway
Allows leeway (default: 5 seconds) during decoding to account for clock skew. Can be configured in a Validation Config. - Time Claim Methods
Shortcut methods like.with_issued_at()and.with_expiration().
Validation ☑️¶
In SuperJWT, validation refers to the process of ensuring that the JWT data—both headers and claims—complies with a predefined structure and set of rules. While verification checks the integrity and authenticity of the token (proving it hasn't been tampered with), validation ensures that the information contained within the token meets your application's requirements, such as required fields, specific data types, or value constraints, typically through the use of Pydantic models.
There are several ways to validate your custom JWT data:
- By using Pydantic directly before encoding or after decoding.
- When using
decode(), claims are validated againstJWTClaimsby default. You can specify a custom model via thevalidationparameter or use a ValidationConfig. - When using
encode(), rawdictclaims are validated againstJWTClaimsby default, and Pydantic claims are validated against their own Pydantic model.
During decoding, claims validation happens after the JWT is verified. Validation can be disabled.
graph LR
HV["`**Headers Validation**
*(HeadersValidationError)*`"] --> S["`🔏 **Signature Verification**
*(SignatureVerificationError)*`"]
S --> CV["`✔️ **Claims Validation**
*(ClaimsValidationError)*`"]
During encoding, claims validation happens before the JWT is signed. Validation can be disabled.
graph LR
CV["`**Claims Validation** ✔️
*(ClaimsValidationError)*`"] --> HV["`**Headers Validation**
*(HeadersValidationError)*`"]
HV --> S["`**JWT Signature** 🔏`"]
Custom Models¶
You can create custom Pydantic models by extending JWTClaims or JWTBaseModel to define your own fields and validation rules. For custom headers, inherit from JOSEHeader. See Pydantic Models.
Claims
from pydantic import AfterValidator, Field, ValidationError
from superjwt import Alg, JWTClaims, JWTDatetimeInt
from superjwt.exceptions import ClaimsValidationError
from typing import Annotated
from uuid import UUID
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# 'exp' is required
exp: JWTDatetimeInt # (1)
# 'sub' is required and its type is changed to integer
sub: int = Field(default=...) # (2)
# 'user_id' is optional and must be a valid UUIDv4 string
user_id: Annotated[str | None, AfterValidator(lambda x: str(UUID(x, version=4)))]
claims = MyJWTClaims.model_construct(
sub=12345, user_id="not-a-uuidv4"
).with_expiration(minutes=15) # (3)
try:
claims.revalidate() # --> ❌ fails
except ValidationError:
claims.user_id = "d4dc7b96-36cc-4ab5-846e-17e4fc85bf6d"
claims.revalidate() # --> ✅ passes
print(claims.to_dict())
#> {'sub': 12345, 'user_id': 'd4dc7b96-36cc-4ab5-846e-17e4fc85bf6d', 'exp': 1767652061}
-
This syntax may trigger your python linter (
"iss" overrides a field of the same name but is missing a default value), see this pyright GitHub issue.
Note thatJWTDatetimeIntis the standard internal type for date/time data in SuperJWT. Internally, it is stored as a Pythondatetime(UTC) and serialized as a UNIX timestamp integer. See Datetime Fields. -
This syntax redefines a field while maintaining linter compatibility.
-
.model_construct()creates a Pydantic instance without running validation.
Claims
from datetime import datetime, UTC
from pydantic import Field
from superjwt import Alg, JWTClaims, JWTDatetimeFloat, decode, encode
from superjwt.exceptions import ClaimsValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# 'nbf' is required and serialized to a float timestamp
nbf: JWTDatetimeFloat = Field(default=...) # (1)
# a new required field
items_id: list[str]
claims = MyJWTClaims.model_construct(
**{
"nbf": datetime(2025, 12, 31, 23, 59, 59, 987654, tzinfo=UTC),
"items_id": ["banana", "apple", 1]
}
)
try:
encode(claims, secret_key, Alg.HS256) # --> ❌ fails (2)
except ClaimsValidationError:
claims.items_id = ["banana", "apple", "orange"]
compact = encode(claims, secret_key, Alg.HS256) # --> ✅ passes (3)
decoded = decode(compact, secret_key, Alg.HS256, validation=MyJWTClaims) # --> ✅ passes
print(decoded.to_dict())
#> {'nbf': 1767225599.987654, 'items_id': ['banana', 'apple', 'orange']} (4)
- Serializes
'nbf'as a float timestamp. - Fails validation because
MyJWTClaimsis used automatically. - Once the
items_iddata is corrected, encoding succeeds. 'nbf'is correctly serialized to a float. See Datetime Fields.
Headers
from pydantic import ValidationError
from superjwt import Alg, JOSEHeader, JWT
secret_key = "your-secret-key-of-len-32-bytes!"
class CustomHeaders(JOSEHeader):
session_id: str
headers = CustomHeaders.make_default(Alg.HS512, session_id="sess-123456")
headers.session_id = 123456
headers.to_dict()
#> {'alg': 'HS512', 'typ': 'JWT', 'session_id': 'sess-123456'}
try:
headers.revalidate() # --> ❌ fails (1)
except ValidationError:
headers.session_id = "sess-123456"
headers.revalidate() # --> ✅ passes
- Because
headersis aCustomHeadersPydantic instance, headers validation runs against its own model. Here,session_idisintand notstr, thus failing.
Headers
from superjwt import Alg, JOSEHeader, JWT
from superjwt.exceptions import HeadersValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
class CustomHeaders(JOSEHeader):
session_id: str
headers = CustomHeaders.make_default(Alg.HS512, session_id="sess-123456")
headers.session_id = 123456
headers.to_dict()
#> {'alg': 'HS512', 'typ': 'JWT', 'session_id': '123456'}
jwt = JWT() # (1)
try:
jwt.encode({}, secret_key, Alg.HS512, headers=headers) # --> ❌ fails (2)
except HeadersValidationError:
headers.session_id = "sess-123456"
compact = jwt.encode({}, secret_key, Alg.HS512, headers=headers).compact # --> ✅ passes
jws_token = jwt.decode(compact, secret_key, Alg.HS512, headers_validation=CustomHeaders)
jws_token.headers
#> {'alg': 'HS512', 'typ': 'JWT', 'session_id': 'sess-123456'}
- We are using a lower-level API here to access the headers data. But it works the same with module-level
encode()anddecode()functions. Warning:JWTis a stateful and non thread-safe object. - Because
headersis a Pydantic instance, headers validation runs against its own Pydantic model. Here,session_idisintand notstr, thus failing.
Examples¶
from pydantic import AfterValidator
from superjwt import Alg, JWTClaims, decode, inspect
from superjwt.exceptions import ClaimsValidationError
from typing import Annotated
from uuid import UUID
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# 'user_id' is required and must be a valid UUIDv4 string
user_id: Annotated[str, AfterValidator(lambda x: str(UUID(x, version=4)))]
valid_compact = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiY2U3OGI4MTMtMjQ0YS00YmRmLWEzNmMtYTc5YjkxOWIyOTY4In0.EEfaVozcCntiHpbuuV2WRGKw1UtLQge2GoJ19HTq_dc"
inspect(valid_compact).payload
#> {'user_id': 'ce78b813-244a-4bdf-a36c-a79b919b2968'} ⚡ (1)
invalid_compact = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibm90LWEtdXVpZC12NCJ9.-DeMZUugR40FDbWBU4nRESczZb5d8UDfuhkTumEeme0"
inspect(invalid_compact).payload
#> {'user_id': 'not-a-uuid-v4'} ⚡ (2)
decode(invalid_compact, secret_key, Alg.HS256) # --> 🤔 passes (3)
#> {'user_id': 'not-a-uuid-v4'}
try:
decode(invalid_compact, secret_key, Alg.HS256, validation=MyJWTClaims) # --> ❌ fails (4)
except ClaimsValidationError:
decoded = decode(
valid_compact, secret_key, Alg.HS256, validation=MyJWTClaims
) # --> ✅ passes
-
Unverified JWT
Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.
-
Unverified JWT
Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.
-
During decoding, claims are validated against
JWTClaimsby default.user_idis not a requirement ofJWTClaims. - The
'user_id'claim is not valid because the value is not a UUIDv4, as required byMyJWTClaims.
from pydantic import Field, ValidationError
from superjwt import Alg, JWTClaims, JWTDatetimeInt, decode, inspect
from typing import Literal
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# redefine 'exp' as required
exp: JWTDatetimeInt = Field(default=...)
# 'permissions' is required and must be a list of string among 3 choices
permissions: list[Literal["user", "dev", "admin"]]
invalid_compact = b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwZXJtaXNzaW9ucyI6WyJkZXYiLCJhbmFseXN0Il0sImV4cCI6MjExMzI2NzI1MH0.ul7aDgO0VQmIKu-7OGpa2qHfXkA6s2XDQuyTA38HDiE"
inspect(invalid_compact).payload
#> {'permissions': ['dev', 'analyst'], 'exp': 2113267250} ⚡ (1)
decoded = decode(invalid_compact, secret_key, Alg.HS256) # --> 🤔 passes (2)
#> {'permissions': ['dev', 'analyst'], 'exp': 2113267250}
decoded_claims = MyJWTClaims.model_construct(**decoded.to_dict())
try:
decoded_claims.revalidate() # --> ❌ fails (3)
except ValidationError:
# token is invalid!
pass
-
Unverified JWT
Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.
-
Without specifying a custom model,
decode()validates againstJWTClaimsby default. Since'permissions'is not a registered claim, it is just an extra field with no validation rules and decoding is successful.
Regardless, the JWT signature is always verified during the decoding process proving its authenticity. - The
'permissions'claim is invalid because'analyst'is not an allowed value in our custom model.
Use a ValidationConfig to specify both a Pydantic model and additional parameters like leeway.
from pydantic import Field
from superjwt import Alg, JWTClaims, ValidationConfig, decode, encode
from superjwt.exceptions import ClaimsValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
sub: str = Field(default=...)
strict = ValidationConfig(
model=MyJWTClaims,
leeway=1.0,
allow_future_iat=False,
)
lenient = ValidationConfig(
model=JWTClaims,
leeway=30.0,
allow_future_iat=True,
)
claims = JWTClaims(iss="my-app")
compact = encode(claims, secret_key, Alg.HS256)
try:
decode(compact, secret_key, Alg.HS256, validation=strict)
except ClaimsValidationError as e:
print(e)
#> Claims validation failed
#> claim ('sub',) -> validation failed (missing): Field required
decoded = decode(compact, secret_key, Alg.HS256, validation=lenient)
print(decoded.iss)
#> 'my-app'
See Encoding Examples
When encoding JWT claims from a Pydantic instance, validation against its own model is automatic.
from pydantic import AfterValidator, Field
from superjwt import Alg, JWTClaims, encode
from superjwt.exceptions import ClaimsValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# redefine existing 'iss' to required integer
iss: int = Field(default=...)
# 'permissions' is required and must be a list of string
permissions: list[str]
valid_claims = MyJWTClaims.model_construct(
**{"permissions": ["user", "admin"], "iss": 1234}
) # (1)
try:
encode(
valid_claims, secret_key, Alg.HS256, validation=JWTClaims # --> ❌ fails (2)
)
except:
compact = encode(valid_claims, secret_key, Alg.HS256) # --> ✅ passes (3)
.model_construct()allows the creation of a Pydantic instance without running validation.- By using
JWTClaimsas the validation model, the claims are no longer compliant because'iss'is an integer instead of a string. - Even though the
validationparameter is not specified, if the input is a Pydantic instance, it is automatically validated against its own model, hereMyJWTClaims.
When encoding JWT claims from a dict, validation runs against JWTClaims.
from superjwt import Alg, JWTClaims, encode
from superjwt.exceptions import ClaimsValidationError
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# 'user_id' is required and must be a valid UUIDv4 string
permissions: list[str]
invalid_claims = MyJWTClaims.model_construct(**{"permissions": [1, "admin"]}) # (1)
try:
encode(invalid_claims, secret_key, Alg.HS256) # ❌ fails (2)
except ClaimsValidationError:
compact = encode(
{"permissions": [1, "admin"]}, secret_key, Alg.HS256 # --> 🤔 passes (3)
)
.model_construct()creates a Pydantic instance without running validation.- Even without
validationparameter specified, Pydantic instances are automatically validated against their own model, hereMyJWTClaims. - During encoding, if claims are passed as a raw
dict, claims validation runs by default againstJWTClaims. Thepermissionsextra field is allowed and has no validation rule, so the encoding is successful.
You can manually validate your claims before encoding your JWT.
from pydantic import AfterValidator, Field, ValidationError
from superjwt import Alg, JWTClaims, JWTDatetimeFloat, encode
from typing import Annotated
from uuid import UUID
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
# 'exp' is required (and will be serialized to float)
exp: JWTDatetimeFloat = Field(default=...)
# 'user_id' is required and must be a valid UUIDv4 string
user_id: Annotated[str, AfterValidator(lambda x: str(UUID(x, version=4)))]
try:
claims = MyJWTClaims.model_construct(**{"user_id": "not-a-uuid-v4"})
claims = claims.with_expiration(minutes=15)
claims.revalidate() # --> ❌ fails (1)
except ValidationError:
claims = MyJWTClaims.model_construct(
**{"user_id":"ce78b813-244a-4bdf-a36c-a79b919b2968"}
)
claims = claims.with_expiration(minutes=15)
claims.revalidate() # --> ✅ passes
compact = encode(claims, secret_key, Alg.HS256)
.revalidate()is aJWTBaseModelmethod that verifies if the instance passes model validation.
Disable Validation¶
You can disable claims validation entirely during encoding or decoding. In this scenario, an expired token will not raise an error. When validation is disabled, decode() returns an instance of JWTBaseModel.
from superjwt import Alg, JWTBaseModel, JWTClaims, Validation, decode, encode
secret_key = "your-secret-key-of-len-32-bytes!"
claims = JWTClaims.model_construct(iss=12345) # 'iss' should be a string
claims_dict = {"iss": 12345} # 'iss' should be a string
# Encode without validation
compact = encode(claims, secret_key, Alg.HS256, validation=Validation.DISABLE) # (1)
compact = encode(claims_dict, secret_key, Alg.HS256, validation=Validation.DISABLE) # (2)
# Decode without validation
decoded: JWTBaseModel = decode(
compact, secret_key, Alg.HS256, validation=Validation.DISABLE
) # (3)
- By default, encoding validates Pydantic claims against its own Pydantic model. Use
Validation.DISABLEto skip validation. - By default, encoding validates raw
dictclaims againstJWTClaims. UseValidation.DISABLEto skip validation. - By default, decoding validates claims in the JWT against
JWTClaims. UseValidation.DISABLEto skip validation.
Note
Even with validation disabled, the signature is always verified when using decode(). To view token content without verification, use the inspect() function.
Validation Config¶
The ValidationConfig class allows you to configure validation by injecting a Pydantic model and additional settings.
| Configuration Argument | Type | Default | Description |
|---|---|---|---|
model |
type[JWTBaseModel] | None |
None |
The Pydantic model to use for validation. |
leeway |
float |
5.0 |
A constant added to iat, nbf, and exp during validation to account for clock skew. |
allow_future_iat |
bool |
False |
If False, validation fails if iat is in the future. |
now |
datetime | None |
None |
Spoof current time with a custom datetime for testing purposes. |
from superjwt import Alg, JWTClaims, ValidationConfig, decode
validation_config = ValidationConfig(
model=JWTClaims,
leeway=60, # 1 minute leeway
allow_future_iat=False
)
decoded = decode(
compact,
secret_key,
Alg.HS256,
validation=validation_config
)
Datetime Fields ⌚¶
JWT Datetime Types¶
In your Pydantic models, fields defined with the type JWTDatetimeInt or JWTDatetimeFloat represent date/time objects and are serialized as UNIX timestamps. They are stored internally as Python datetime and have the following properties:
- Accepts Python
datetimeobjects (with or without timezone). - Accepts
intorfloatUNIX timestamps. - Values are converted to
datetimeobjects when appropriate. - Timestamps are either serialized to an
int(withJWTDatetimeInt) or afloat(withJWTDatetimeFloat).
Note
All date/time registered claims ('iat', 'exp' and 'nbf') are defined as JWTDatetimeInt type in JWTClaims Pydantic model. They can be redefined as JWTDatetimeFloat with a custom model.
from datetime import datetime
from pydantic import Field
from superjwt import Alg, JWTClaims, JWTDatetimeInt, JWTDatetimeFloat, encode, inspect
secret_key = "your-secret-key-of-len-32-bytes!"
class MyJWTClaims(JWTClaims):
exp: JWTDatetimeFloat = Field(default=...)
custom_time: JWTDatetimeInt
claims = MyJWTClaims.model_construct(
custom_time=datetime(2025, 12, 31, 23, 59, 59, 987654)
).with_expiration(days=1)
compact = encode(claims, secret_key, Alg.HS256)
print(inspect(compact).payload)
#> {'exp': 1768257699.998604, 'custom_time': 1767225599} (1)
'exp'timestamp is serialized to afloatbecause it was redefined asJWTDatetimeFloat.'custom_time'timestamp is anintbecause it is defined asJWTDatetimeInt.
Spoof Time¶
You can override the current time with a custom datetime object for testing purposes. All time integrity and validity checks will account for the new spoofed "present time". Spoofing is especially useful to test 'nbf' claim during decoding.
from datetime import datetime
from superjwt import Alg, JWTClaims, ValidationConfig, decode, inspect
from superjwt.exceptions import TokenNotYetValidError
secret_key = "your-secret-key-of-len-32-bytes!"
compact = (
b"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
b"eyJzdWIiOiJ1c2VyLTEyMyIsIm5iZiI6MTk4NzY1NDMyMX0."
b"6GIisgWqqeiIMslavC51QYGWqIrxnKXKVrhIlV7W8XA"
)
print(inspect(compact).payload)
#> {'sub': 'user-123', 'nbf': 1987654321} --> Not valid before December 26, 2032
validation_spoof_before = ValidationConfig(
model=JWTClaims,
now=datetime(2032, 12, 21)
)
validation_spoof_after = ValidationConfig(
model=JWTClaims,
now=datetime(2032, 12, 29)
)
try:
decode(
compact, secret_key, Alg.HS256, validation=validation_spoof_before
) # --> ❌ fails (1)
except TokenNotYetValidError as e:
print(e)
#> Token is no yet valid
decoded = decode(
compact, secret_key, Alg.HS256, validation=validation_spoof_after
) # --> ✅ passes (2)
- The "present time" is five days before
'nbf', the token is not yet valid. - The "present time" is three days after
'nbf', the token is valid.
from datetime import datetime
from superjwt import Alg, JWTClaims, encode
from superjwt.exceptions import TokenExpiredError
secret_key = "your-secret-key-of-len-32-bytes!"
claims = JWTClaims(sub="user-123").with_expiration(days=7)
# Set current time to a future date
claims.spoof_time(
datetime(2046, 12, 31, 23, 59, 59)
)
try:
encode(claims, secret_key, Alg.HS256)
except TokenExpiredError as e:
print(e)
#> Token has expired
Asymmetric Algorithms 🔓¶
Unlike HMAC algorithm which uses the same key for encoding and decoding, asymmetric algorithms use a private key to sign tokens and a corresponding public key to verify them.
Why Use Asymmetric Algorithms?
In this scenario, the private key never needs to be shared, while the public key can be distributed widely to many verifiers. This enables scalable architectures (multiple services can verify tokens without access to private keys), easier key rotation and auditability, and support for robust algorithms (RSA, ECDSA, EdDSA) suited for cross‑service and third‑party integrations.
See Pros & Cons (Asymmetric)
Encode With a Private Key¶
from superjwt import Alg, RSAKey, decode, encode
private_pem = b"-----BEGIN PRIVATE KEY-----\nMIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGB...JKcY1op6RcnYLA/h2XmxWxMgJa9eqL8/s0tk\ncAQ2NIRpUOJf\n-----END PRIVATE KEY-----\n"
key = RSAKey.import_key(private_pem)
compact = encode({"sub": "user123"}, key, Alg.RS256)
decoded = decode(compact, key, Alg.RS256) # (1)
- When importing a private key, the public key component is derived automatically. You can use then this key instance for both encoding and decoding.
from superjwt import Alg, ECKey, decode, encode
private_pem = b"-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49A...qzlKhJzG\n-----END PRIVATE KEY-----\n"
key = ECKey.import_key(private_pem)
compact = encode({"sub": "user123"}, key, Alg.ES256)
decoded = decode(compact, key, Alg.ES256) # (1)
- When importing a private key, the public key component is derived automatically. You can use then this key instance for both encoding and decoding.
from superjwt import Alg, OKPKey, decode, encode
private_pem = b"-----BEGIN PRIVATE KEY-----\nMEcCAQAwBQYDK2VxBDsEOWe...Yr\n+y4nNmgf3BsBvc3wRKPdfaO4dya2CJLLnA==\n-----END PRIVATE KEY-----\n"
key = OKPKey.import_key(private_pem)
compact = encode({"sub": "user123"}, key, Alg.Ed25519)
decoded = decode(compact, key, Alg.Ed25519) # (1)
- When importing a private key, the public key component is derived automatically. You can use then this key instance for both encoding and decoding.
See How to generate keys.
Decode with a Public Key¶
from superjwt import Alg, RSAKey, decode
public_pem = b"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w...IDAQAB\n-----END PUBLIC KEY-----\n"
key = RSAKey.import_public_key(public_pem)
decoded = decode(compact, key, Alg.RS256) # (1)
- You can only decode a JWT with a public key, not create one.
Detached Payload 🕊️¶
You can make a JWT with a detached payload, meaning the claims are not embedded in the compact token. Useful for bandwidth optimization when the payload is transmitted through a separate secure channel or when it is too large to be exchanged through HTTP headers.
from superjwt import Alg, JWTClaims, decode, encode
secret_key = "your-secret-key-of-len-32-bytes!"
claims = JWTClaims(sub="user123", iss="myapp").with_expiration(minutes=30)
claims_dict = claims.to_dict()
#> {'iss': 'myapp', 'sub': 'user123', 'exp': 1767715523}
compact = encode(claims, secret_key, Alg.HS256, detach_payload=True)
#> b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
# .. (1)
# wNZgGwkVGw_Rble80j2J6a9IylXa5jgq4EO33XiEa4g'
decoded = decode(
compact,
secret_key,
Alg.HS256,
with_detached_payload=claims_dict, # (2)
validation=JWTClaims
)
#> {'iss': 'myapp', 'sub': 'user123', 'exp': 1767715523}
- Note that the encoded payload is empty!
- The claims payload was transferred separately and is needed to perform the JWT verification. Remember the JWT signing input is the
.-concatenated encoded Base64Url headers with encoded Base64Url claims.