Skip to content

JWTModel

JWTModel

Bases: BaseModel

A Pydantic model that is also a JWT.

Declare the claims as fields and set the keys in model_config; the model then both issues tokens (generate(), jwt_str) and validates incoming ones (from_token(), or by validating a token string into the field).

Reading a token string verifies its signature; building a model from a dict does not, so never treat a model built from request data as authenticated. Set verified_only=True to have the model reject payloads outright and construct it deliberately with from_claims().

Unknown claims are rejected by default (extra='forbid'); set extra='ignore' for tokens from third-party issuers.

Examples:

from pydantic_jwt import ConfigDict, Exp, JWTModel, after


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

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


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

jwt_str property

jwt_str: JWTStr

Return the signed token as a JWTStr.

Signs on every access, exactly like generate(), and wraps the result so the header, payload and signature can be inspected.

from_token classmethod

from_token(
    jwt_str: str,
    *,
    decoding_key: str | None = None,
    algorithm: str | None = None,
    require_keys: bool | None = None,
    context: Any | None = None,
) -> T

Parse a token string, validate its claims and verify its signature.

Runs in three steps: structural parse, payload validation, then signature verification. A token that fails claim validation never reaches the signature check.

The payload comes from a token, so it is exempt from the verified_only guard.

Parameters:

Name Type Description Default
jwt_str str

The compact token to read.

required
decoding_key str | None

Key to verify with, overriding model_config.

None
algorithm str | None

Algorithm to verify with, overriding model_config. It is never taken from the token's own alg header, which is what prevents algorithm-confusion attacks.

None
require_keys bool | None

Overrides model_config for this call.

None
context Any | None

Validation context forwarded to model_validate(). Pass {'validate_claims': False} to skip the claim markers.

None

Returns:

Type Description
T

The validated model.

Raises:

Type Description
ValidationError

The payload is malformed or a claim was rejected.

PydanticCustomError

The signature does not verify (jwt_invalid_signature), or no key is available and require_keys is on (jwt_missing_key).

Source code in pydantic_jwt/base.py
@classmethod
def from_token(
    cls: type[T],
    jwt_str: str,
    *,
    decoding_key: str | None = None,
    algorithm: str | None = None,
    require_keys: bool | None = None,
    context: Any | None = None,
) -> T:
    """Parse a token string, validate its claims and verify its signature.

    Runs in three steps: structural parse, payload validation, then signature
    verification. A token that fails claim validation never reaches the
    signature check.

    The payload comes from a token, so it is exempt from the
    `verified_only` guard.

    Args:
        jwt_str: The compact token to read.
        decoding_key: Key to verify with, overriding `model_config`.
        algorithm: Algorithm to verify with, overriding `model_config`. It is
            never taken from the token's own `alg` header, which is what
            prevents algorithm-confusion attacks.
        require_keys: Overrides `model_config` for this call.
        context: Validation context forwarded to `model_validate()`. Pass
            `{'validate_claims': False}` to skip the claim markers.

    Returns:
        The validated model.

    Raises:
        ValidationError: The payload is malformed or a claim was rejected.
        PydanticCustomError: The signature does not verify
            (`jwt_invalid_signature`), or no key is available and
            `require_keys` is on (`jwt_missing_key`).
    """

    jwt_obj = JWTStr(jwt_str)
    instance = cls.model_validate(jwt_obj.payload, context={**(context or {}), "allow_unverified_payload": True})
    instance._verify_signature(jwt_str, decoding_key, algorithm, require_keys)
    return instance

from_claims classmethod

from_claims(
    context: dict[str, Any] | None = None, /, **claims: Any
) -> T

Build a model from claim values, without a token to verify.

This is how a verified_only=True model is constructed on the issuing side: it marks the payload as deliberate, so the guard that rejects unverified payloads stands down. Everything else still runs — field types, claim markers and extra handling — so the result is a model you are about to sign, never an authenticated one.

On a model without verified_only it is simply model_validate() over keyword arguments.

Parameters:

Name Type Description Default
context dict[str, Any] | None

Positional-only validation context, merged into the one this method passes. Use it for {'validate_claims': False} when building a deliberately expired token in a test.

None
**claims Any

The claim values, as keyword arguments.

{}

Returns:

Type Description
T

The validated model.

Raises:

Type Description
ValidationError

A field or claim was rejected.

Examples:

token = AccessToken.from_claims(sub='user-42')
raw = token.generate()

stale = AccessToken.from_claims({'validate_claims': False}, sub='user-42', exp=1)
Source code in pydantic_jwt/base.py
@classmethod
def from_claims(cls: type[T], context: dict[str, Any] | None = None, /, **claims: Any) -> T:
    """Build a model from claim values, without a token to verify.

    This is how a `verified_only=True` model is constructed on the issuing
    side: it marks the payload as deliberate, so the guard that rejects
    unverified payloads stands down. Everything else still runs — field
    types, claim markers and `extra` handling — so the result is a model you
    are about to sign, never an authenticated one.

    On a model without `verified_only` it is simply `model_validate()` over
    keyword arguments.

    Args:
        context: Positional-only validation context, merged into the one
            this method passes. Use it for `{'validate_claims': False}` when
            building a deliberately expired token in a test.
        **claims: The claim values, as keyword arguments.

    Returns:
        The validated model.

    Raises:
        ValidationError: A field or claim was rejected.

    Examples:
        ```python
        token = AccessToken.from_claims(sub='user-42')
        raw = token.generate()

        stale = AccessToken.from_claims({'validate_claims': False}, sub='user-42', exp=1)
        ```
    """
    return cls.model_validate(claims, context={**(context or {}), "allow_unverified_payload": True})

generate

generate(
    *,
    encoding_key: str | None = None,
    algorithm: str | None = None,
) -> str

Encode the model as a signed token.

The payload is model_dump(mode='json'), so custom field serialisers decide what lands in the token.

Parameters:

Name Type Description Default
encoding_key str | None

Key to sign with, overriding model_config. Useful during key rotation.

None
algorithm str | None

Algorithm to sign with, overriding model_config.

None

Returns:

Type Description
str

The compact, signed token.

Raises:

Type Description
PydanticCustomError

No key or algorithm is available (jwt_missing_key).

Source code in pydantic_jwt/base.py
def generate(self, *, encoding_key: str | None = None, algorithm: str | None = None) -> str:
    """Encode the model as a signed token.

    The payload is `model_dump(mode='json')`, so custom field serialisers
    decide what lands in the token.

    Args:
        encoding_key: Key to sign with, overriding `model_config`. Useful
            during key rotation.
        algorithm: Algorithm to sign with, overriding `model_config`.

    Returns:
        The compact, signed token.

    Raises:
        PydanticCustomError: No key or algorithm is available
            (`jwt_missing_key`).
    """

    if encoding_key is None:
        encoding_key = self.model_config.get("encoding_key")
    if algorithm is None:
        algorithm = self.model_config.get("algorithm")

    if encoding_key is None or algorithm is None:
        raise PydanticCustomError(
            "jwt_missing_key",
            "encoding_key and algorithm must be set in model_config to generate a token",
            {"model": self.__class__.__name__, "keys": "encoding_key, algorithm"},
        )

    payload = self.model_dump(mode="json")
    return jwt.encode(payload, encoding_key, algorithm=algorithm)