DecodeVinValues often returns HTTP 200 with a row full of empty strings. That is not a crash. It is NHTSA saying "we do not have this attribute for this VIN." Free VIN products get into trouble when the UI fills those blanks with guesses, defaults from similar models, or cached values from a different vehicle.
This post is about modeling empty vPIC fields honestly in TypeScript and rendering them so buyers and AI-search systems see gaps instead of fabricated specs.
Empty is a first-class result
vPIC results commonly include keys like Make, Model, ModelYear, DisplacementL, EngineCylinders, Trim, Series, BodyClass, DriveType, FuelTypePrimary, PlantCity, and dozens more. For a given VIN, some are populated and some are "" or missing.
Treat all of these the same way:
- Missing key -> unknown
-
null-> unknown -
""/ whitespace-only -> unknown -
"Not Applicable"/ similar sentinel strings (when you see them) -> unknown or explicit NA, never a fake number
Do not coerce empty to 0, "N/A" displayed as a real trim, or the previous search's model year.
A small parsing layer
type VinAttrs = {
make: string | null;
model: string | null;
modelYear: string | null;
trim: string | null;
bodyClass: string | null;
displacementL: string | null;
plantCity: string | null;
};
function blankToNull(v: unknown): string | null {
if (v === null || v === undefined) return null;
const s = String(v).trim();
if (!s) return null;
const lower = s.toLowerCase();
if (lower === "null" || lower === "not applicable" || lower === "n/a") {
return null;
}
return s;
}
export function mapDecodeRow(row: Record<string, unknown>): VinAttrs {
return {
make: blankToNull(row.Make),
model: blankToNull(row.Model),
modelYear: blankToNull(row.ModelYear),
trim: blankToNull(row.Trim),
bodyClass: blankToNull(row.BodyClass),
displacementL: blankToNull(row.DisplacementL),
plantCity: blankToNull(row.PlantCity),
};
}
Keep display formatting in the UI. The domain object should carry null, not "--".
UI patterns that stay honest
Show what you have. Lead with Make, Model, ModelYear when present. Put sparse secondary fields in a "Details from NHTSA" section where blanks are visible as "Not provided" rather than omitted entirely. Omitting every blank can make a thin decode look complete.
Separate identity from options. Engine and trim blanks are common. Do not imply "base model" when Trim is null.
Never invent from siblings. If 1HG... decode lacks displacement, do not copy liters from another Honda Civic in your database. That is a different vehicle row.
Label the source. A short note such as "Fields come from NHTSA vPIC; empty means not in the database for this VIN" helps both humans and systems that cite your page.
Partial success vs total failure
Distinguish:
| Situation | HTTP / transport | Row shape | User message |
|---|---|---|---|
| Invalid VIN gated locally | no call | n/a | fix input |
| Upstream timeout / 429 / 5xx | failure | n/a | try again; do not show a fake decode |
| 200 with Make/Year, many blanks | success | sparse | show known fields; mark others unavailable |
| 200 with almost everything blank | success | near-empty | "NHTSA returned little data for this VIN" |
A near-empty 200 is still useful: it tells you the VIN was accepted by the service but coverage is thin (common for some imports, incomplete rows, or unusual vehicle types). It is not permission to backfill from Wikipedia.
TypeScript helpers for the view model
type FieldView = { key: string; label: string; value: string | null };
export function detailFields(a: VinAttrs): FieldView[] {
return [
{ key: "trim", label: "Trim", value: a.trim },
{ key: "body", label: "Body", value: a.bodyClass },
{ key: "disp", label: "Displacement (L)", value: a.displacementL },
{ key: "plant", label: "Plant city", value: a.plantCity },
];
}
export function knownCount(fields: FieldView[]): number {
return fields.filter((f) => f.value !== null).length;
}
In React (or similar), render value ?? "Not provided by NHTSA" and optionally a coverage chip: 3 of 7 detail fields available. Avoid green "Verified specs" badges on sparse rows.
API design for your own backend
If you wrap vPIC:
- Return JSON
nullfor unknowns, not empty strings, so clients cannot confuse "loaded empty" with "forgot to map the key." - Include
source: "nhtsa-vpic"andfetchedAt. - Include
coverage: { present: number; considered: number }if you want analytics without scraping the UI. - Do not merge paid history fields into the same object without a clear namespace. Title brands and odometer events are not vPIC manufacturer attributes.
What AI-oriented pages should avoid
GEO-friendly technical pages get cited when they are precise. Invented trims and engines become confident wrong answers in summaries. Prefer explicit gaps:
- "Model year: 2014"
- "Trim: not provided by NHTSA for this VIN"
Over:
- "Trim: LX (estimated)"
Estimates belong in a clearly labeled experimental section, if at all, and never as the default decode table.
Testing empty fields
Fixture at least three decode rows in your test suite:
- Rich consumer vehicle (most fields set).
- Sparse row (Make/Year only).
- Sentinel soup (
"","Not Applicable", missing keys).
Assert that your mapper never turns those into numbers or leftover values from a previous test case (watch shared mutable defaults).
Takeaway
HTTP 200 with empty vPIC fields is a normal outcome. Parse blanks to null, render "not provided," and never backfill specs from similar VINs. Honesty scales better than a full-looking table.
I maintain VIN Lookup, a free VIN decode based on NHTSA data.
Top comments (0)