Skip to content

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'}
  1. When encoding from a raw dict, the claims are automatically validated against JWTClaims to 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
  1. .model_construct() creates a Pydantic instance without validating its model. This allows for the creation of an invalid Pydantic instance without raising a pydantic.ValidationError.
  2. During encoding, if the claims object is a Pydantic instance, validation runs automatically based on its own Pydantic model. Since 'jti' is not a str, a ClaimsValidationError is 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}
  1. Without a custom Pydantic model, you cannot pass a Python datetime object 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}
  1. We passed a Python datetime to the JWTDatetime field '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)
  1. We have added the Issued At claim. In the JWTClaims Pydantic instance, the value is stored as a Python datetime (UTC). It is automatically serialized as a timestamp in the JSON output.
  2. 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)
  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)
  1. Even though we manually passed the 'exp' claim as a float, the claims is validated against JWTClaims. 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'
  1. Unverified JWT

    Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT. Only the decode() function guarantees integrity.

  2. The token was successfully verified and validated against the JWTClaims Pydantic model. Use the validation parameter 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!

  1. 😱 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}

  1. Unverified JWT

    Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.

  2. By default, decode() validates claims against JWTClaims. Since 'iss' must be a string, validation fails.

  3. To decode without validation, explicitly use Validation.DISABLE. The JWT is still verified, proving its authenticity. The returning Pydantic instance is a JWTBaseModel, 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}
  1. 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}
  1. By creating a ValidationConfig instance to be passed to validation parameter, you can change other behaviors like the leeway when decoding 'iat', 'exp', 'nbf' claims.
  2. 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.

  3. The 'iat' and 'exp' claims are stored as a Python datetime object. 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
  1. .model_construct() creates a Pydantic instance without running validation.
  2. Since 'exp' is in the past, the token fails JWTClaims validation.
  3. 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 Python dict. This is similar to pydantic.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 all JWTDatetime timestamps to be integers instead of floats (default: True).
    See Validation Config.
  • Auto-Revalidation
    Automatically revalidates Pydantic instances (equivalent to revalidate_instances="always").

JOSEHeader

Inherits from JWTBaseModel and defines a compliant set of protected headers (JOSE Header).

Properties:

  • Protected Header Values
    Defines a mandatory alg field and other optional fields such as 'typ'='JWT', 'kid', and 'crit'.
  • Default Headers Model for Validation
    Headers are validated against this model when headers_validation is not set in decode().
  • Make Default Method
    Creates header data with the required 'alg' field populated.

    from superjwt import Alg, JOSEHeader
    
    headers = JOSEHeader.make_default(Alg.ES256)
    #> {'alg': 'ES256', 'typ': 'JWT'}
    

  • 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', optional str
    • 'sub', optional str
    • 'aud', optional str or list[str]
    • 'iat', optional JWTDatetime
    • 'nbf', optional JWTDatetime
    • 'exp', optional JWTDatetime
    • 'jti', optional str
    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 when validation parameter is not set in decode().

  • Time Integrity Checks
    Ensures the following conditions are met for 'iat', 'nbf', and 'exp':
    • 'iat' < now (can be disabled with allow_future_iat, see Validation Config)
    • 'nbf' < now
    • 'exp' > now
  • Token Time Validity
    Raises TokenExpiredError if the token has expired (given 'exp' claim timestamp) or TokenNotYetValidError if 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 against JWTClaims by default. You can specify a custom model via the validation parameter or use a ValidationConfig.
  • When using encode(), raw dict claims are validated against JWTClaims by 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}
  1. 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 that JWTDatetimeInt is the standard internal type for date/time data in SuperJWT. Internally, it is stored as a Python datetime (UTC) and serialized as a UNIX timestamp integer. See Datetime Fields.

  2. This syntax redefines a field while maintaining linter compatibility.

  3. .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)
  1. Serializes 'nbf' as a float timestamp.
  2. Fails validation because MyJWTClaims is used automatically.
  3. Once the items_id data is corrected, encoding succeeds.
  4. '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
  1. Because headers is a CustomHeaders Pydantic instance, headers validation runs against its own model. Here, session_id is int and not str, 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'}
  1. We are using a lower-level API here to access the headers data. But it works the same with module-level encode() and decode() functions. Warning: JWT is a stateful and non thread-safe object.
  2. Because headers is a Pydantic instance, headers validation runs against its own Pydantic model. Here, session_id is int and not str, 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
  1. Unverified JWT

    Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.

  2. Unverified JWT

    Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.

  3. During decoding, claims are validated against JWTClaims by default. user_id is not a requirement of JWTClaims.

  4. The 'user_id' claim is not valid because the value is not a UUIDv4, as required by MyJWTClaims.
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
  1. Unverified JWT

    Token inspection DOES NOT verify the signature! Never trust information from an unverified JWT.

  2. Without specifying a custom model, decode() validates against JWTClaims by 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.

  3. 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)
  1. .model_construct() allows the creation of a Pydantic instance without running validation.
  2. By using JWTClaims as the validation model, the claims are no longer compliant because 'iss' is an integer instead of a string.
  3. Even though the validation parameter is not specified, if the input is a Pydantic instance, it is automatically validated against its own model, here MyJWTClaims.

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)
    )
  1. .model_construct() creates a Pydantic instance without running validation.
  2. Even without validation parameter specified, Pydantic instances are automatically validated against their own model, here MyJWTClaims.
  3. During encoding, if claims are passed as a raw dict, claims validation runs by default against JWTClaims. The permissions extra 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)
  1. .revalidate() is a JWTBaseModel method 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)
  1. By default, encoding validates Pydantic claims against its own Pydantic model. Use Validation.DISABLE to skip validation.
  2. By default, encoding validates raw dict claims against JWTClaims. Use Validation.DISABLE to skip validation.
  3. By default, decoding validates claims in the JWT against JWTClaims. Use Validation.DISABLE to 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 datetime objects (with or without timezone).
  • Accepts int or float UNIX timestamps.
  • Values are converted to datetime objects when appropriate.
  • Timestamps are either serialized to an int (with JWTDatetimeInt) or a float (with JWTDatetimeFloat).

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)
  1. 'exp' timestamp is serialized to a float because it was redefined as JWTDatetimeFloat.

    'custom_time' timestamp is an int because it is defined as JWTDatetimeInt.

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)
  1. The "present time" is five days before 'nbf', the token is not yet valid.
  2. 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)
  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)
  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)
  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)
  1. You can only decode a JWT with a public key, not create one.
from superjwt import Alg, ECKey, decode

public_pem = b"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKo...jnBeBPp/f8HA==\n-----END PUBLIC KEY-----\n"

key = ECKey.import_public_key(public_pem)

decoded = decode(compact, key, Alg.ES256)  # (1)
  1. You can only decode a JWT with a public key, not create one.
from superjwt import Alg, OKPKey, decode

public_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwA...VotMRLDwHw=\n-----END PUBLIC KEY-----\n"

key = OKPKey.import_public_key(public_pem)

decoded = decode(compact, key, Alg.Ed25519)  # (1)
  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}
  1. Note that the encoded payload is empty!
  2. 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.