Fix entry edit always failing with 'Input should be None'

Two bugs:

1. Python 3.12 name collision in schemas.py: `date: Optional[date] = None`
   caused get_type_hints() to resolve the `date` type annotation to NoneType
   (Optional[None]) because the field name shadowed the datetime.date import.
   All *Update schemas were rejecting any PUT with a valid date. Fixed by
   aliasing the import: `from datetime import date as Date`.

2. FastAPI validation errors return detail as a list of objects, not a string.
   Passing that list to new Error() produced "Error: [object Object]". Fixed
   in api.js to map the detail array to msg strings before throwing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 20:37:15 -07:00
parent dcfc605579
commit d2afdc4ea3
2 changed files with 17 additions and 14 deletions

View File

@@ -1,4 +1,4 @@
from datetime import date, datetime from datetime import date as Date, datetime
from decimal import Decimal from decimal import Decimal
from typing import Optional from typing import Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -56,18 +56,18 @@ class UserOut(BaseModel):
# ── Egg Collections ─────────────────────────────────────────────────────────── # ── Egg Collections ───────────────────────────────────────────────────────────
class EggCollectionCreate(BaseModel): class EggCollectionCreate(BaseModel):
date: date date: Date
eggs: int = Field(ge=0) eggs: int = Field(ge=0)
notes: Optional[str] = None notes: Optional[str] = None
class EggCollectionUpdate(BaseModel): class EggCollectionUpdate(BaseModel):
date: Optional[date] = None date: Optional[Date] = None
eggs: Optional[int] = Field(default=None, ge=0) eggs: Optional[int] = Field(default=None, ge=0)
notes: Optional[str] = None notes: Optional[str] = None
class EggCollectionOut(BaseModel): class EggCollectionOut(BaseModel):
id: int id: int
date: date date: Date
eggs: int eggs: int
notes: Optional[str] notes: Optional[str]
created_at: datetime created_at: datetime
@@ -78,18 +78,18 @@ class EggCollectionOut(BaseModel):
# ── Flock History ───────────────────────────────────────────────────────────── # ── Flock History ─────────────────────────────────────────────────────────────
class FlockHistoryCreate(BaseModel): class FlockHistoryCreate(BaseModel):
date: date date: Date
chicken_count: int = Field(ge=0) chicken_count: int = Field(ge=0)
notes: Optional[str] = None notes: Optional[str] = None
class FlockHistoryUpdate(BaseModel): class FlockHistoryUpdate(BaseModel):
date: Optional[date] = None date: Optional[Date] = None
chicken_count: Optional[int] = Field(default=None, ge=0) chicken_count: Optional[int] = Field(default=None, ge=0)
notes: Optional[str] = None notes: Optional[str] = None
class FlockHistoryOut(BaseModel): class FlockHistoryOut(BaseModel):
id: int id: int
date: date date: Date
chicken_count: int chicken_count: int
notes: Optional[str] notes: Optional[str]
created_at: datetime created_at: datetime
@@ -100,20 +100,20 @@ class FlockHistoryOut(BaseModel):
# ── Feed Purchases ──────────────────────────────────────────────────────────── # ── Feed Purchases ────────────────────────────────────────────────────────────
class FeedPurchaseCreate(BaseModel): class FeedPurchaseCreate(BaseModel):
date: date date: Date
bags: Decimal = Field(gt=0, decimal_places=2) bags: Decimal = Field(gt=0, decimal_places=2)
price_per_bag: Decimal = Field(gt=0, decimal_places=2) price_per_bag: Decimal = Field(gt=0, decimal_places=2)
notes: Optional[str] = None notes: Optional[str] = None
class FeedPurchaseUpdate(BaseModel): class FeedPurchaseUpdate(BaseModel):
date: Optional[date] = None date: Optional[Date] = None
bags: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2) bags: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
price_per_bag: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2) price_per_bag: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
notes: Optional[str] = None notes: Optional[str] = None
class FeedPurchaseOut(BaseModel): class FeedPurchaseOut(BaseModel):
id: int id: int
date: date date: Date
bags: Decimal bags: Decimal
price_per_bag: Decimal price_per_bag: Decimal
notes: Optional[str] notes: Optional[str]
@@ -125,18 +125,18 @@ class FeedPurchaseOut(BaseModel):
# ── Other Purchases ─────────────────────────────────────────────────────────── # ── Other Purchases ───────────────────────────────────────────────────────────
class OtherPurchaseCreate(BaseModel): class OtherPurchaseCreate(BaseModel):
date: date date: Date
total: Decimal = Field(gt=0, decimal_places=2) total: Decimal = Field(gt=0, decimal_places=2)
notes: Optional[str] = None notes: Optional[str] = None
class OtherPurchaseUpdate(BaseModel): class OtherPurchaseUpdate(BaseModel):
date: Optional[date] = None date: Optional[Date] = None
total: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2) total: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
notes: Optional[str] = None notes: Optional[str] = None
class OtherPurchaseOut(BaseModel): class OtherPurchaseOut(BaseModel):
id: int id: int
date: date date: Date
total: Decimal total: Decimal
notes: Optional[str] notes: Optional[str]
created_at: datetime created_at: datetime

View File

@@ -13,7 +13,10 @@ const API = {
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText })); const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || `Request failed (${res.status})`); const detail = Array.isArray(err.detail)
? err.detail.map(e => e.msg).join(', ')
: err.detail;
throw new Error(detail || `Request failed (${res.status})`);
} }
if (res.status === 204) return null; // DELETE returns No Content if (res.status === 204) return null; // DELETE returns No Content
return res.json(); return res.json();