modelgen: recurse through nested generics in list resolvers

_get_list_inner (Pydantic) and _resolve_ts_list (TypeScript) collapsed
nested generics to "str" / "string[]" — so List[List[int]] became
List[str] and List[Dict[str,Any]] became List[str]. Fix recurses on
list-of-list and falls back to Dict[str,Any] / Record<string,unknown>
for list-of-dict.

Regenerated outputs:
- core/gpu/models/models.py: SegmentFieldResponse.boundary now
  List[List[int]], unblocking /segment_field which was 500ing on every
  request with pydantic ValidationError
- core/db/models.py + ui/common/types/generated.ts: Brand.airings now
  matches its source schema (List[Dict[str,Any]] / Record<string,unknown>[])
This commit is contained in:
2026-04-29 08:48:21 -03:00
parent f66d3a273f
commit ddb4f17faa
4 changed files with 18 additions and 6 deletions

View File

@@ -164,7 +164,7 @@ class Brand(SQLModel, table=True):
aliases: List[str] = Field(default_factory=list, sa_column=Column(JSON, nullable=False, server_default='[]'))
source: BrandSource = "ocr"
confirmed: bool = False
airings: List[str] = Field(default_factory=list, sa_column=Column(JSON, nullable=False, server_default='[]'))
airings: List[Dict[str, Any]] = Field(default_factory=list, sa_column=Column(JSON, nullable=False, server_default='[]'))
total_airings: int = 0
created_at: Optional[datetime] = Field(default_factory=datetime.utcnow)
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow)

View File

@@ -115,13 +115,13 @@ class SegmentFieldRequest(BaseModel):
class SegmentFieldResponse(BaseModel):
"""Response from field segmentation."""
boundary: List[str] = Field(default_factory=list)
boundary: List[List[int]] = Field(default_factory=list)
coverage: float = 0.0
mask_b64: str = ""
class SegmentFieldDebugResponse(BaseModel):
"""Response from field segmentation with debug overlay."""
boundary: List[str] = Field(default_factory=list)
boundary: List[List[int]] = Field(default_factory=list)
coverage: float = 0.0
mask_overlay_b64: str = ""

View File

@@ -8,6 +8,8 @@ Used by generators to convert Python types to target framework types.
import dataclasses as dc
from typing import Any, Callable, get_args
from .helpers import get_origin_name
# =============================================================================
# Django Type Mappings
# =============================================================================
@@ -38,7 +40,7 @@ DJANGO_SPECIAL: dict[str, str] = {
def _get_list_inner(type_hint: Any) -> str:
"""Get inner type of List[T] for Pydantic."""
"""Get inner type of List[T] for Pydantic. Recurses for nested generics."""
args = get_args(type_hint)
if args:
inner = args[0]
@@ -46,6 +48,11 @@ def _get_list_inner(type_hint: Any) -> str:
return {str: "str", int: "int", float: "float", bool: "bool"}[inner]
if isinstance(inner, type) and dc.is_dataclass(inner):
return inner.__name__
origin = get_origin_name(inner)
if origin == "list":
return f"List[{_get_list_inner(inner)}]"
if origin == "dict":
return "Dict[str, Any]"
return "str"
@@ -69,7 +76,7 @@ PYDANTIC_RESOLVERS: dict[Any, Callable[[Any], str]] = {
def _resolve_ts_list(base: Any) -> str:
"""Resolve TypeScript list type."""
"""Resolve TypeScript list type. Recurses for nested generics."""
args = get_args(base)
if args:
inner = args[0]
@@ -81,6 +88,11 @@ def _resolve_ts_list(base: Any) -> str:
return "boolean[]"
elif isinstance(inner, type) and dc.is_dataclass(inner):
return f"{inner.__name__}[]"
origin = get_origin_name(inner)
if origin == "list":
return f"{_resolve_ts_list(inner)}[]"
if origin == "dict":
return "Record<string, unknown>[]"
return "string[]"

View File

@@ -117,7 +117,7 @@ export interface Brand {
aliases: string[];
source: BrandSource;
confirmed: boolean;
airings: string[];
airings: Record<string, unknown>[];
total_airings: number;
created_at: string | null;
updated_at: string | null;