diff --git a/src/zammad_mcp_server/models.py b/src/zammad_mcp_server/models.py index d98acce..36b7b18 100644 --- a/src/zammad_mcp_server/models.py +++ b/src/zammad_mcp_server/models.py @@ -4,7 +4,7 @@ from enum import Enum from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import AliasChoices, BaseModel, ConfigDict, Field, field_validator class TicketState(str, Enum): @@ -50,7 +50,7 @@ class Visibility(str, Enum): class StateBrief(BaseModel): """Brief state information.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int name: str @@ -60,7 +60,7 @@ class StateBrief(BaseModel): class PriorityBrief(BaseModel): """Brief priority information.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int name: str @@ -69,7 +69,7 @@ class PriorityBrief(BaseModel): class GroupBrief(BaseModel): """Brief group information.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int name: str @@ -78,7 +78,7 @@ class GroupBrief(BaseModel): class UserBrief(BaseModel): """Brief user information.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int firstname: str | None = None @@ -90,7 +90,7 @@ class UserBrief(BaseModel): class OrganizationBrief(BaseModel): """Brief organization information.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int name: str @@ -99,10 +99,10 @@ class OrganizationBrief(BaseModel): class Article(BaseModel): """Zammad article (ticket message/comment).""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int - ticket_id: int = Field(alias="ticketId") + ticket_id: int = Field(validation_alias=AliasChoices("ticket_id", "ticketId")) type: str | None = None sender: str | None = None subject: str | None = None @@ -128,7 +128,7 @@ def parse_user_field(cls, v: Any) -> Any: class Ticket(BaseModel): """Zammad ticket model.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int number: str | None = None @@ -191,7 +191,7 @@ def get_group_name(self) -> str | None: class User(BaseModel): """Zammad user model.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int login: str | None = None @@ -225,7 +225,7 @@ def get_full_name(self) -> str: class Organization(BaseModel): """Zammad organization model.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int name: str @@ -242,7 +242,7 @@ class Organization(BaseModel): class Group(BaseModel): """Zammad group model.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) id: int name: str @@ -256,7 +256,7 @@ class Group(BaseModel): class GroupCreateRequest(BaseModel): """Request model for creating a group.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) name: str = Field(..., min_length=1, max_length=500) active: bool = True @@ -266,7 +266,7 @@ class GroupCreateRequest(BaseModel): class TicketStats(BaseModel): """Ticket statistics.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) total: int open: int @@ -281,7 +281,7 @@ class TicketStats(BaseModel): class SearchResult(BaseModel): """Generic search result wrapper.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) items: list[Any] total_count: int @@ -293,7 +293,7 @@ class SearchResult(BaseModel): class TicketCreateRequest(BaseModel): """Request model for creating a ticket.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) title: str = Field(..., min_length=1, max_length=500) group: str = Field(..., min_length=1) @@ -309,7 +309,7 @@ class TicketCreateRequest(BaseModel): class TicketUpdateRequest(BaseModel): """Request model for updating a ticket.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) title: str | None = Field(None, max_length=500) group: str | None = None @@ -322,7 +322,7 @@ class TicketUpdateRequest(BaseModel): class ArticleCreateRequest(BaseModel): """Request model for creating an article.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) ticket_id: int = Field(..., gt=0) subject: str | None = Field(None, max_length=500) @@ -336,7 +336,7 @@ class ArticleCreateRequest(BaseModel): class UserCreateRequest(BaseModel): """Request model for creating a user.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) login: str | None = None firstname: str | None = None @@ -352,7 +352,7 @@ class UserCreateRequest(BaseModel): class OrganizationCreateRequest(BaseModel): """Request model for creating an organization.""" - model_config = ConfigDict(extra="allow") + model_config = ConfigDict(extra="allow", populate_by_name=True) name: str = Field(..., min_length=1, max_length=500) shared: bool = True diff --git a/tests/test_models.py b/tests/test_models.py index a3d03a7..e18b2fb 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -10,6 +10,7 @@ ArticleCreateRequest, ArticleType, Group, + GroupBrief, Organization, OrganizationCreateRequest, PriorityBrief, @@ -156,6 +157,27 @@ def test_article_internal_default(self) -> None: assert article.internal is False + def test_article_accepts_zammad_snake_case_ticket_id(self) -> None: + """Test article parsing from Zammad API response format.""" + article = Article.model_validate({ + "id": 1, + "ticket_id": 100, + "body": "This is the article body", + }) + + assert article.ticket_id == 100 + + def test_article_accepts_camel_case_ticket_id(self) -> None: + """Test article parsing remains compatible with camelCase ticketId.""" + article = Article.model_validate({ + "id": 1, + "ticketId": 100, + "body": "This is the article body", + }) + + assert article.ticket_id == 100 + + class TestOrganizationModel: """Test suite for Organization model."""