Skip to content

Import dry run green-lights a row the write then rejects: structured value shapes (address / location) are not pre-checked #4633

Description

@os-zhuang

Found while authoring HotCRM's first mapping artifacts (objectstack-ai/hotcrm#603). Not blocking that work — filing so the divergence is recorded rather than rediscovered.

What happens

A CSV cell aimed at a structured address (json-backed) field passes the dry run and fails the real write.

Measured on 17.0.0-rc.1, POST /api/v1/data/crm_account/import, mapping Billing Address to billing_address (a Field.address):

dryRun: true, runAutomations: false

{ "total": 1, "ok": 1, "errors": 0, "created": 1,
  "results": [ { "row": 1, "ok": true, "action": "created" } ] }

the same payload, dryRun omitted

{ "total": 1, "ok": 0, "errors": 1, "created": 0,
  "results": [ { "row": 1, "ok": false, "action": "failed",
    "code": "VALIDATION_FAILED",
    "error": "Billing Address has an invalid address value: Invalid input: expected object, received string" } ] }

Nothing is corrupted — the engine rejects the string per row, loudly and readably. The problem is only that the dry run promised otherwise.

Why it matters

The dry run's stated contract is that it predicts the verdict the real write produces. import-coerce.ts says so twice, and both firstMissingRequiredField and firstConstraintViolation exist specifically to close that gap:

> Mirrors the numeric-range and string-length rules of the engine's validateRecord — same type applicability, same comparison, same code and message text — so the import's dry run predicts the verdict the real write produces (framework#3956). Before this, a dry run only reported coercion failures … so -500 in a min: 0 column was reported valid and then rejected by the write.

The engine's value-shape validation (valueShapeStrictFor / mediaValueShapeStrictFor) is a third such rule, and it has no pre-check counterpart. coerceCell routes address / location through its final catch-all — "Everything else (text, email, phone, json, html, single file, …): pass through" — so no verdict is formed at all.

This is precisely the "false all-clear" #3956 set out to eliminate, and it lands on the user who did the right thing: dry-run a 5,000-row file, get a clean report, run it, watch a column's worth of rows fail.

Suggested shape

Add a value-shape pre-check beside firstConstraintViolation, rendering the same code / message the engine's own rejection renders, so the two read identically. Scope it to the same "engine's OWN list" discipline the bounds check already follows — only the types validateRecord actually shape-checks — so it does not start false-alarming on types the write accepts.

Not the same thing

Composing an address object from separate Street / City / Postcode columns is a feature the mapping spec does not have (no object-building transform), and that is a defensible gap, not this bug. This issue is only about the dry run and the write disagreeing on the payload a caller did send.


裁决后状态(2026-08-06,cli 车道 PM 按裁决 D 拆分): 本单收缩为 cli 消费半边(rest/import 走新 validate-only 操作 + import-coerce.ts 手抄预检镜像退役);契约半边(DataProtocol 新增 validate-only 操作,spec 先行)已拆出 #6037

Blocked-by: #6037

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions