pythonjwtpydanticpydantic-v2

I'm trying to create a Pydantic extension that allows me to deserialize jwt strings into pydantic models


I'm trying to deserialize JWT strings directly into nested Pydantic models, but I’m encountering a validation issue when passing the JWT string to a nested model field. Specifically, I’m using Pydantic's @model_validator (in "before" mode) to decode the JWT and merge its payload with the original values before validation.

The goal is to decode the JWT string, extract its payload, and then allow Pydantic to populate the corresponding fields in the nested model. However, when I use Pydantic's model_validate method to validate the model, it throws a ValidationError, stating that a string cannot be passed to a nested model.

Here is my code:

from typing import Dict, Any
import jwt
from pydantic import BaseModel, model_validator


class JwtDecodeError(Exception):
    """Custom exception for JWT decoding errors."""
    pass


class JwtBaseModel(BaseModel):
    session_jwt: str

    @classmethod
    @model_validator(mode="before")
    def decode_jwt(cls, values: Dict[str, Any]) -> Dict[str, Any]:
        """Decodes the JWT token without requiring a signature."""
        token = values.get("session_jwt")
        if not token:
            raise ValueError("JWT token is required")

        try:
            # Decode the JWT without signature verification
            decoded_payload = jwt.decode(token, options={"verify_signature": False})
        except jwt.DecodeError as e:
            raise JwtDecodeError(f"Failed to decode JWT token: {str(e)}")

        # Merge the decoded payload with values to pass it to the Pydantic model fields
        values.update(decoded_payload)
        return values


class UsernameSignature(JwtBaseModel):
    valid: bool
    busy: bool
    exp: int
    payment_required: bool


class Username(BaseModel):
    busy: bool
    meta: dict
    payment_required: bool
    signature: UsernameSignature
    valid: bool
    username: str


model_json = {
    "busy": False,
    "meta": {
        "gameSessionId": "",
        "tracing": None
    },
    "payment_required": True,
    "signature": "eyJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6ImpkamRvd2RiIiwidmFsaWQiOnRydWUsImJ1c3kiOmZhbHNlLCJleHAiOjE3MjY3NzY1OTksInBheW1lbnRfcmVxdWlyZWQiOnRydWV9.VjCpW2rY8MEgcVKJrx9Igz05-OnPdJuZxmg4wFUuy25Ixik02O9BkqXoOrfdgo0QR_-BPzB1AdAoMoveu3eqVHwtKGOXzG__eU6kObOlcowqB7iu9SwfK6TXmh1djaxhCCOveOIcj3UOlXQIZ4b6JuFSkezzHAUfaBe2KZskhyFH107MPxLnwWnVL3jOJwOcM7ierd96Y1xyDqmBlL9k8hotTWHwsNlTi0H2GjXWdMk8c54eMGIBhma66q_S__LQ9k1VSZkIj1awOpPzBrUdxMNw9B6J1bsPN8ajLIrLM_oHrXWGDGpIK9Clmbi6XfPe87X09pKutRapKU5nCqOWog",
    "username": "jdjdowdb",
    "valid": True
}

parsed_model = Username.model_validate(model_json)

The error I get

pydantic_core._pydantic_core.ValidationError: 1 validation error for Username
signature
  Input should be a valid dictionary or instance of UsernameSignature [type=model_type, input_value='eyJhbGciOiJSUzI1NiJ9.eyJ...e87X09pKutRapKU5nCqOWog', input_type=str]
    For further information visit https://errors.pydantic.dev/2.9/v/model_type

Solution

  • Note that I had to modify your JWT decoding syntax a bit, you may be running a different version of JWT.

    from typing import Any, Dict
    
    from jwt import JWT
    from jwt.exceptions import JWTDecodeError
    from pydantic import BaseModel, model_validator
    
    
    class JwtDecodeError(Exception):
        """Custom exception for JWT decoding errors."""
    
        pass
    
    
    class DeserializeJWT(BaseModel):
        decoded_data: dict
    
        @model_validator(mode="before")
        @classmethod
        def decode_jwt(cls, signature: str) -> Dict[str, Any]:
            instance = JWT()
    
            try:
                decoded = instance.decode(signature, do_verify=False)
            except JWTDecodeError as e:
                raise JwtDecodeError(f"Failed to decode JWT token: {str(e)}")
            return {"signature": signature, "decoded_data": decoded}
    
    
    class Decoded(DeserializeJWT):
        signature: str
    
    
    class Username(BaseModel):
        busy: bool
        meta: dict
        payment_required: bool
        valid: bool
        username: str
        signature: Decoded
    
    
    model_json = {
        "busy": False,
        "meta": {"gameSessionId": "", "tracing": None},
        "payment_required": True,
        "signature": "your_jwt",
        "username": "jdjdowdb",
        "valid": True,
    }
    
    parsed_model = Username.model_validate(model_json)