NHTSA vPIC often returns Series, Trim, and sometimes Trim2 on DecodeVinValues-style payloads. When they are populated -- "EX-L", "Limited", "Sport", a series code -- they help buyers see how the manufacturer labeled the pattern. When they are blank, product teams feel the gap. The temptation is to upgrade emptiness into a nicer badge: scrape a marketing site, map BodyClass to "Luxury Trim," or default missing Series to "Base" so the card never looks incomplete.
This post is about honest display: pass through Series/Trim when vPIC provides them, refuse blank-to-upgrade inventing, and allow a clean "not provided" state when the fields are empty.
What Series and Trim are (and are not)
Series and Trim are catalog / manufacturer attributes associated with the VIN decode. They answer a narrow question: which series or trim label did the decode associate with this vehicle pattern?
They do not answer:
- Whether the used car still has that package's options after dealer swaps
- MSRP, residual value, or "fully loaded" status
- Option codes that never appear in vPIC
- Lifestyle copy ("adventure ready," "executive suite")
- A safe default when both fields are empty
Empty Series and Trim does not mean "Base." It means vPIC did not give you those labels. Do not invent "Base," "Standard," or "SE" from make/model folklore so the UI looks finished.
Normalize empties, do not invent upgrades
Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Do not coalesce missing Trim into Series with a slash upgrade, and do not fill gaps from BodyClass or DriveType.
const EMPTY = new Set([
"",
"not applicable",
"n/a",
"na",
"null",
"none",
"unknown",
]);
export type SeriesTrimView = {
series: string | null;
trim: string | null;
trim2: string | null;
source: "vpic" | "missing";
};
function clean(raw: string | null | undefined): string | null {
const t = (raw ?? "").trim();
if (!t || EMPTY.has(t.toLowerCase())) return null;
return t;
}
export function viewSeriesTrim(fields: {
Series?: string | null;
Trim?: string | null;
Trim2?: string | null;
}): SeriesTrimView {
const series = clean(fields.Series);
const trim = clean(fields.Trim);
const trim2 = clean(fields.Trim2);
if (!series && !trim && !trim2) {
return { series: null, trim: null, trim2: null, source: "missing" };
}
return { series, trim, trim2, source: "vpic" };
}
export function seriesTrimLines(view: SeriesTrimView): string[] {
if (view.source === "missing") {
return ["Series / trim: not provided by vPIC for this VIN"];
}
const lines: string[] = [];
if (view.series) lines.push(`Series (vPIC): ${view.series}`);
if (view.trim) lines.push(`Trim (vPIC): ${view.trim}`);
if (view.trim2) lines.push(`Trim2 (vPIC): ${view.trim2}`);
lines.push("Catalog labels only -- not a package upgrade or MSRP tier");
return lines;
}
Show only rows that have values. A blank Trim next to a present Series is fine -- do not invent a matching trim noun so both columns look "complete."
Forbidden upgrades
Product pressure often asks for:
- Defaulting empty Trim to
"Base"or"Standard" - Mapping BodyClass ("Sedan") to "Luxury Sedan Trim" when Series/Trim are blank
- Scraping OEM configurators to "complete" the card after a sparse decode
- Concatenating Make + Model + invented adjectives into one "Package" chip
- Treating Trim2 as a premium upsell badge when it is just another catalog string
Refuse those. Partial truth beats confident fiction. If Series is present and Trim is not, show Series alone. If both are missing, say so once -- do not leave a glowing empty chip that implies a mid-tier package.
export function assertNoTrimUpgrades(moduleSource: string): void {
const banned = [
/default.*base/i,
/luxury\s+sedan\s+trim/i,
/fully\s+loaded/i,
/executive\s+suite/i,
/scrape.*configurator/i,
];
for (const re of banned) {
if (re.test(moduleSource)) {
throw new Error(
`Series/Trim display must not invent upgrades: ${re}`,
);
}
}
}
export function cardRows(fields: {
Series?: string | null;
Trim?: string | null;
Trim2?: string | null;
BodyClass?: string | null;
}): { label: string; value: string }[] {
const view = viewSeriesTrim(fields);
const body = clean(fields.BodyClass);
const rows: { label: string; value: string }[] = [];
if (view.series) rows.push({ label: "Series (vPIC)", value: view.series });
if (view.trim) rows.push({ label: "Trim (vPIC)", value: view.trim });
if (view.trim2) rows.push({ label: "Trim2 (vPIC)", value: view.trim2 });
if (view.source === "missing") {
rows.push({ label: "Series / trim (vPIC)", value: "not provided" });
}
if (body) rows.push({ label: "Body class (vPIC)", value: body });
return rows;
}
Keep BodyClass on its own row. Never merge body class into a fake trim name inside the mapper.
Display order and duplication
When Series and Trim repeat the same token, you may show both (faithful to vPIC) or dedupe for UI brevity -- but dedupe must not invent a third "combined package" string. Prefer:
- Series row, Trim row, Trim2 row as sourced
- One "not provided" line when all three are empty
Do not sort empties to the top with placeholder "---" that looks like a mid-grade trim code.
Small tests that lock honesty
import assert from "node:assert/strict";
assert.equal(viewSeriesTrim({ Series: "EX-L", Trim: "" }).series, "EX-L");
assert.equal(viewSeriesTrim({ Series: "EX-L", Trim: "" }).trim, null);
assert.equal(viewSeriesTrim({ Series: "N/A", Trim: "n/a" }).source, "missing");
assert.ok(
seriesTrimLines(viewSeriesTrim({})).some((l) => /not provided/i.test(l)),
);
assert.ok(
!seriesTrimLines(viewSeriesTrim({})).some((l) => /Base|Standard|Luxury/i.test(l)),
);
const rows = cardRows({
Series: "",
Trim: "",
BodyClass: "Sedan",
});
assert.ok(rows.some((r) => r.value === "not provided"));
assert.ok(rows.some((r) => r.label.includes("Body class")));
assert.ok(!rows.some((r) => /Luxury Sedan Trim/i.test(r.value)));
Add a review rule: the series/trim module must not contain default-Base logic or BodyClass-to-trim maps except in tests that forbid them.
Takeaway
Series, Trim, and Trim2 are catalog labels. Display them with clear sourcing, allow "not provided," and never turn blanks into upgrades. Your free VIN UI stays trustworthy when trim is either a real vPIC string with a modest footnote -- or absent -- and body class stays on its own labeled row.
I maintain VIN Lookup, a free VIN decode based on NHTSA data.
Top comments (0)