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
No fields configured for Enhancement.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions