Skip to content

Fixed the messy auto-generated serde logic in the library #23

@mridang

Description

@mridang

Title: Refactor Python SDK Models to Standardize Pydantic Usage via ZitadelModel Base Class

Description:
This initiative will refactor the Python SDK's model classes, which already use Pydantic, to standardize their implementation, remove boilerplate, and leverage Pydantic V2 features more directly via a common ZitadelModel base class.

Problem:

The current Python models, while using Pydantic, contain generated boilerplate code:

  • Custom to_json, from_json, to_dict, from_dict methods that partially replicate or deviate from standard Pydantic V2 functionality.
  • An additional_properties field and associated logic (__properties) that may not be necessary and complicates parsing/serialization.
  • Lack of a common base class for shared configuration and helper methods.
    This leads to unnecessary code duplication and potential inconsistencies with idiomatic Pydantic V2 usage.

Impact:

Refactoring will result in:

  • Slimmer, cleaner, and more maintainable Pydantic models.
  • Consistent use of Pydantic V2's efficient model_dump, model_dump_json, model_validate, and model_validate_json methods.
  • Removal of potentially confusing or unnecessary custom serialization/deserialization logic and additional_properties handling.
  • Improved developer experience.

Solution / Tasks:

1. Implement ZitadelModel Base Class:
Create a class ZitadelModel(BaseModel) that serves as the base for all SDK models. It should define the common Pydantic configuration and provide standardized serialization/deserialization methods.

Example zitadel_model.py:

from pydantic import BaseModel, ConfigDict
from typing import Any, Dict, Optional, TypeVar, Type
# Use typing_extensions for Self if Python < 3.11
from typing_extensions import Self

# Define a TypeVar for the class type
T = TypeVar('T', bound='ZitadelModel')

class ZitadelModel(BaseModel):
    model_config = ConfigDict(
        populate_by_name=True,      # Allow using JSON field names (aliases)
        validate_assignment=True,   # Validate fields on assignment
        protected_namespaces=(),    # Standard Pydantic setting
        extra='ignore',             # Ignore unexpected fields in JSON input
                                  # Use 'forbid' to raise an error instead
    )

    def to_zitadel_dict(self, by_alias: bool = True, exclude_unset: bool = True, exclude_none: bool = False) -> Dict[str, Any]:
        """Standard dictionary representation using Pydantic V2."""
        return self.model_dump(
            mode='python',
            by_alias=by_alias,
            exclude_unset=exclude_unset,
            exclude_none=exclude_none
        )

    def to_zitadel_json(self, by_alias: bool = True, exclude_unset: bool = True, exclude_none: bool = False) -> str:
        """Standard JSON representation using Pydantic V2."""
        return self.model_dump_json(
            by_alias=by_alias,
            exclude_unset=exclude_unset,
            exclude_none=exclude_none
        )

    @classmethod
    def from_zitadel_dict(cls: Type[T], obj: Optional[Dict[str, Any]]) -> Optional[T]:
        """Create an instance from a dictionary using Pydantic V2."""
        if obj is None:
            return None
        return cls.model_validate(obj)

    @classmethod
    def from_zitadel_json(cls: Type[T], json_str: str) -> Optional[T]:
        """Create an instance from a JSON string using Pydantic V2."""
        return cls.model_validate_json(json_str)

    # Optional: Keep a consistent debug string representation if needed
    def to_str(self) -> str:
        """Returns the string representation of the model using alias for debugging."""
        import pprint
        return pprint.pformat(self.model_dump(by_alias=True))

2. Update OpenAPI Generator Templates for Python Models:
Modify the OpenAPI Generator templates for Python models to:

  • Make generated models inherit from ZitadelModel.
  • Define model fields using standard Python type hints (Optional, List, etc.) and Pydantic's Field for aliases (e.g., user_id: Optional[str] = Field(default=None, alias="userId")).
  • Remove the additional_properties field and the __properties class variable.
  • Remove the custom to_json, from_json, to_dict, from_dict methods from the generated models (they will be inherited).
  • Ensure enums are generated as standard Python enum.Enum classes, which Pydantic handles well.

Target structure for a generated model (e.g., user_service_user.py):

from __future__ import annotations
from typing import List, Optional
from pydantic import Field
# Assuming these imports point to other refactored ZitadelModel children or Enums
from .zitadel_model import ZitadelModel
from .user_service_details import UserServiceDetails
from .user_service_human_user import UserServiceHumanUser
from .user_service_machine_user import UserServiceMachineUser
from .user_service_user_state import UserServiceUserState # Assuming this is an Enum

class UserServiceUser(ZitadelModel):
    """
    UserServiceUser (Refactored)
    """
    user_id: Optional[str] = Field(default=None, alias="userId")
    details: Optional[UserServiceDetails] = None
    state: Optional[UserServiceUserState] = Field(default=UserServiceUserState.USER_STATE_UNSPECIFIED)
    username: Optional[str] = None
    login_names: Optional[List[str]] = Field(default=None, alias="loginNames")
    preferred_login_name: Optional[str] = Field(default=None, alias="preferredLoginName")
    human: Optional[UserServiceHumanUser] = None
    machine: Optional[UserServiceMachineUser] = None

    # No custom methods, no additional_properties, no __properties

3. Update SDK Code to Use New Model Methods:

  • Search the SDK codebase (e.g., api_client.py, service files) for any remaining calls to the old custom to_json, from_json, to_dict, from_dict methods.
  • Replace them with calls to the standardized methods inherited from ZitadelModel (e.g., instance.to_zitadel_json(), ModelClass.from_zitadel_json(data)).
  • Ensure the API client uses these methods correctly when preparing request bodies and parsing responses.

Expected Outcomes:

  • Python SDK models are significantly leaner, inheriting configuration and core ser/des methods from ZitadelModel.
  • Serialization and deserialization consistently use Pydantic V2's model_dump* and model_validate* methods.
  • Boilerplate related to custom ser/des methods and additional_properties is removed from models.
  • The SDK aligns better with idiomatic Pydantic V2 usage.
  • Functional equivalence for JSON-based API interactions is preserved.

Additional Notes:

  • Dependencies: Ensure pydantic>=2.0 is specified.
  • Null Handling: Review the desired behavior for omitting fields vs. including explicit null. The exclude_unset=True (default in example) and exclude_none=False (default in example) parameters in the base class methods control this. Adjust as needed.
  • Testing: Thorough testing of serialization/deserialization for various model types is essential.
  • extra='ignore' vs 'forbid': Decide if unknown fields in incoming JSON should be silently ignored or raise a validation error.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions