DEV Community

Vin Lookup
Vin Lookup

Posted on

Displaying Doors from vPIC Without Inventing Body Marketing Names

NHTSA vPIC often returns a Doors field on DecodeVinValues-style payloads: a short catalog value such as 2, 3, 4, or 5. That number is useful on a free VIN decode card. The trap is turning it into a body marketing name -- "coupe," "hatchback lifestyle," "crew cab luxury" -- that the doors field never asserted.

This post is about honest display: show the door count when vPIC provides it, refuse body-style inventing, and allow a clean "not provided" state when the field is empty. Pair Doors with BodyClass only as separate rows, never as a merged slogan.

What Doors is (and is not)

Doors answers a narrow catalog question: how many doors did the decode associate with this vehicle pattern?

It does not answer:

  • Whether the vehicle is a coupe, sedan, hatch, or wagon in marketing language
  • Whether a rear hatch counts as a "fifth door" in your brand glossary
  • Cab configuration on pickups (regular / extended / crew) when that lives in other fields
  • Whether a used car still has the same door count after body work
  • Trim packages, "4-door luxury," or lifestyle copy

Empty Doors does not mean "probably 4." It means vPIC did not give you a value. Do not invent a default from make/model folklore or from BodyClass alone.

Normalize empties, do not invent body names

Treat blank, "Not Applicable", "N/A", and similar tokens as missing. Do not map 2 to "Sport Coupe" or 4 to "Family Sedan."

const EMPTY = new Set([
  "",
  "not applicable",
  "n/a",
  "na",
  "null",
  "none",
  "unknown",
]);

export type DoorsView = {
  count: string | null;
  source: "vpic" | "missing";
};

export function viewDoors(fields: {
  Doors?: string | null;
}): DoorsView {
  const raw = (fields.Doors ?? "").trim();
  if (!raw || EMPTY.has(raw.toLowerCase())) {
    return { count: null, source: "missing" };
  }
  // Keep the catalog token; do not coerce into marketing nouns.
  return { count: raw, source: "vpic" };
}

export function doorsLines(view: DoorsView): string[] {
  if (view.source === "missing") {
    return ["Doors: not provided by vPIC for this VIN"];
  }
  return [
    `Doors (vPIC): ${view.count}`,
    "Door count only -- not a body style or trim name",
  ];
}
Enter fullscreen mode Exit fullscreen mode

The footnote matters. Buyers and bots both over-read short numbers. A second line that names the source and the limit keeps the UI honest without burying the useful value.

Forbidden upgrades

Product pressure often asks for:

  1. Mapping 2 to a "Coupe" badge and 4 to "Sedan" without reading BodyClass
  2. Synonym tables that turn door counts into lifestyle chips ("urban runabout," "family hauler")
  3. Inferring doors from body class, trim text, or photo heuristics when Doors is blank
  4. Showing "usually 4 doors on this model" from a static brochure file
  5. Merging Doors + BodyClass into one string like "4-Door Sport Utility"

Refuse those. If you show body class, show it on its own row from BodyClass. If you show doors, show the catalog count. Never concatenate them into a marketing noun phrase inside the decode mapper.

export function assertNoDoorMarketing(moduleSource: string): void {
  const banned = [
    /sport\s*coupe/i,
    /family\s*sedan/i,
    /lifestyle/i,
    /crew\s*cab\s*luxury/i,
    /urban\s*runabout/i,
  ];
  for (const re of banned) {
    if (re.test(moduleSource)) {
      throw new Error(`Door display module must not invent body marketing: ${re}`);
    }
  }
}

export function cardRows(fields: {
  Doors?: string | null;
  BodyClass?: string | null;
}): { label: string; value: string }[] {
  const doors = viewDoors(fields);
  const body = (fields.BodyClass ?? "").trim();
  const rows: { label: string; value: string }[] = [];

  if (doors.source === "vpic" && doors.count) {
    rows.push({ label: "Doors (vPIC)", value: doors.count });
  } else {
    rows.push({ label: "Doors (vPIC)", value: "not provided" });
  }

  if (body && !EMPTY.has(body.toLowerCase())) {
    rows.push({ label: "Body class (vPIC)", value: body });
  }

  return rows;
}
Enter fullscreen mode Exit fullscreen mode

Separate rows keep attribution clear. A single "Style" chip that secretly combines doors and body class is how marketing sneaks past review.

Pickup and hatch edge cases

Door counts on trucks and hatchbacks invite brochure language (crew cab, coupe, liftgate as a "fifth door"). Resist. If vPIC says 4, show 4. Label cab-related fields separately when they exist. Never count doors from listing photos in the same row as the NHTSA value.

Small tests that lock honesty

import assert from "node:assert/strict";

assert.equal(viewDoors({ Doors: "4" }).count, "4");
assert.equal(viewDoors({ Doors: "N/A" }).source, "missing");
assert.equal(viewDoors({ Doors: "" }).source, "missing");

assert.ok(
  doorsLines(viewDoors({ Doors: "2" })).some((l) => /not a body style/i.test(l)),
);
assert.ok(
  !doorsLines(viewDoors({ Doors: "2" })).some((l) => /coupe|sedan|luxury/i.test(l)),
);

const rows = cardRows({
  Doors: "4",
  BodyClass: "Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV)",
});
assert.equal(rows.length, 2);
assert.ok(rows[0].label.includes("Doors"));
assert.ok(rows[1].label.includes("Body class"));
assert.ok(!rows.some((r) => /4-Door Sport/i.test(r.value)));
Enter fullscreen mode Exit fullscreen mode

Add a review rule: the doors module must not contain body marketing nouns except in tests that forbid them. For chips, show the number with a "Doors" label -- never upgrade 5 to "Hatchback" (that belongs in BodyClass).

Takeaway

Doors is a catalog count. Display it with clear sourcing, allow "not provided," and never use it as a key into invented body marketing names. Your free VIN UI stays trustworthy when door count is either a real vPIC value with a modest footnote -- or absent -- and body class lives on its own labeled row.

I maintain VIN Lookup, a free VIN decode based on NHTSA data.

Top comments (0)