From d2afdc4ea3035db92a82e0f1273ecb4201ca914a Mon Sep 17 00:00:00 2001 From: derekc Date: Sun, 22 Mar 2026 20:37:15 -0700 Subject: [PATCH] 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 --- backend/schemas.py | 26 +++++++++++++------------- nginx/html/js/api.js | 5 ++++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/backend/schemas.py b/backend/schemas.py index 07eaeb1..62b4754 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -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 diff --git a/nginx/html/js/api.js b/nginx/html/js/api.js index b32c783..dfdecd6 100644 --- a/nginx/html/js/api.js +++ b/nginx/html/js/api.js @@ -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();