daybook/issues/008-source-fields-untyped-and-mangled.md
peter 09ea0a8b4f Reject non-string source_name/source_id/source_prompt (issue #008)
These accepted any JSON type on both POST and PATCH, silently mangled
into Python repr() strings at save time (a dict became the literal text
"{'a': 1}", unparseable as JSON) - and the immediate response echoed the
raw pre-save value, while a later GET returned the mangled one, an
internally inconsistent API.

Added validate_string_fields(), rejecting non-string values with 400
before they reach save(), matching #005's approach for tags. Verified live
and with new tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:27:07 +01:00

3.2 KiB

008 — source_name/source_id/source_prompt accept any JSON type and get silently mangled into repr() strings

Status: Fixed

Follow-up to the #007 handover, which specifically asked to probe other wrong-typed PATCH fields beyond tags now that #005 is fixed.

Description

Unlike tags, metadata, and tag_colors (all now type-checked, tags since #005), source_name, source_id, and source_prompt accept any JSON type — object, array, number — on both POST (source_fields() in views.py) and PATCH (ArtifactDetailView.patch). Nothing validates they're strings before assignment.

source_name/source_id are CharField, source_prompt is TextField. Django's Field.get_prep_value() silently coerces non-string input via plain str(value) at save time — which for a dict/list produces Python repr() syntax, not JSON: {"a": 1} becomes the string "{'a': 1}" (single quotes, unparseable as JSON). This is lossy and irreversible — there's no way to recover the original structured value.

Worse: the API response is internally inconsistent. Immediately after a POST/PATCH with a non-string value, the response echoes back the raw, pre-save Python object (still a real JSON object/array in the response body) — but a subsequent GET of the same artifact returns the mangled repr() string instead. Same request, two different apparent types for the same field depending only on when you read it.

Reproduced:

# PATCH source_name with an int, source_id with a list, source_prompt with a dict
curl -X PATCH .../api/artifacts/a74de286 -d '{"source_name": 12345}' ...
# -> 200, response shows "source_name": 12345 (int)

curl .../api/artifacts/a74de286   # fresh GET, session auth
# -> "source_name": "12345" (string)

# Actual DB values (confirmed via shell):
# source_name = '12345'
# source_id   = "['a', 'b']"          <- repr() of the list, not JSON
# source_prompt = "{'nested': 'object'}"  <- repr() of the dict, not JSON

Also reproduced identically on POST (create), not just PATCH — a freshly created artifact's immediate response shows "source_name": {"a": 1} (real object) even though what's stored (and what any later GET returns) is the mangled string form.

Suggested Fix

Add a type check for source_name, source_id, and source_prompt (must be str or absent/null) alongside the existing tags/metadata/ tag_colors checks in validate_source_fields (create path) and ArtifactDetailView.patch (update path) — return 400 naming the field, consistent with #005's approach for tags. This also removes the response/DB inconsistency, since invalid types would never reach save() in the first place.

Resolution

Added validate_string_fields(data, fields) in artifacts/views.py, rejecting any non-None, non-str value with 400. Called from validate_source_fields (covers POST, both text and binary paths) and directly in ArtifactDetailView.patch for source_name, source_id, source_prompt — matching #005's approach for tags. Invalid values never reach save(), so the response/DB inconsistency the issue described can no longer occur. Verified live against the running dev instance and with new tests in tests/api/test_artifacts.py.