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

View File

@@ -13,7 +13,10 @@ const API = {
if (!res.ok) {
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
return res.json();