DEV Community

Vin Lookup
Vin Lookup

Posted on

Displaying BodyClass from vPIC Without Inventing Trim Packages

NHTSA vPIC returns a field many UIs want to surface immediately: BodyClass (and related body-style style fields depending on endpoint shape). It is useful. It is also easy to oversell. Teams turn Sedan/Saloon into "Sport Touring Premium" or map Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV) into a trim ladder the decode never contained.

This post is about honest display: show BodyClass as factory taxonomy, keep trim language out of the decode panel, and write TypeScript that refuses to invent packages when the API is blank or generic.

What BodyClass actually is

In DecodeVinValues-style responses, BodyClass is a standardized category string from the vPIC catalog. Think broad architecture: sedan, coupe, pickup, SUV/MPV, convertible, van, and similar buckets. It answers "what kind of body is this vehicle built as?" It does not answer "which dealer brochure package was sold."

Trim, series, and option packages live in manufacturer marketing. Some of that sometimes appears in other vPIC fields (for example Series or Trim when populated). Often those fields are empty. Empty does not mean "guess from BodyClass."

The overclaim pattern to avoid

// Bad: marketing copy derived only from BodyClass
function fakeTrim(bodyClass: string): string {
  if (/SUV/i.test(bodyClass)) return "Adventure Package";
  if (/Sedan/i.test(bodyClass)) return "Luxury Sedan Trim";
  return "Standard";
}
Enter fullscreen mode Exit fullscreen mode

That function will ship in a PR someday if you do not ban it in review. It makes comparison pages look rich and makes buyers angry when the door sticker disagrees.

Pass through, normalize lightly, label clearly

Prefer a display helper that cleans whitespace and unknown sentinels without rewriting meaning.

export type BodyClassView = {
  label: string | null;
  source: "vpic";
};

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

export function viewBodyClass(raw: string | null | undefined): BodyClassView {
  if (raw == null) return { label: null, source: "vpic" };
  const cleaned = raw.replace(/\s+/g, " ").trim();
  if (!cleaned || EMPTY.has(cleaned.toLowerCase())) {
    return { label: null, source: "vpic" };
  }
  return { label: cleaned, source: "vpic" };
}

export function bodyClassLine(view: BodyClassView): string {
  if (!view.label) return "Body class: not provided by NHTSA for this VIN";
  return `Body class: ${view.label}`;
}
Enter fullscreen mode Exit fullscreen mode

In the UI, keep the heading factual: "Body class (NHTSA)" not "Style and trim." If you also have a non-empty Trim or Series field from the same decode, show it on its own row with its own label. Never concatenate BodyClass + invented adjectives into one "Package" chip.

Combine fields without merging concepts

export type DecodeSlice = {
  BodyClass?: string | null;
  Trim?: string | null;
  Series?: string | null;
};

export function bodyAndTrimRows(d: DecodeSlice): Array<{ k: string; v: string }> {
  const rows: Array<{ k: string; v: string }> = [];
  const body = viewBodyClass(d.BodyClass ?? null);
  rows.push({
    k: "Body class",
    v: body.label ?? "Not provided",
  });

  const trim = (d.Trim ?? "").trim();
  const series = (d.Series ?? "").trim();
  if (trim) rows.push({ k: "Trim (vPIC)", v: trim });
  if (series) rows.push({ k: "Series (vPIC)", v: series });
  if (!trim && !series) {
    rows.push({
      k: "Trim / series",
      v: "Not provided by this decode -- check the window sticker",
    });
  }
  return rows;
}
Enter fullscreen mode Exit fullscreen mode

The explicit empty row matters. Marketplaces that hide missing trim imply the decode proved a base model. Silence is not evidence.

Styling tips that stay honest

  • Use BodyClass for icons or filters (sedan vs pickup) if you need facets; document that facets are coarse
  • Do not sort inventory by "luxury score" derived from BodyClass alone
  • When BodyClass is long and parenthetical, show the full NHTSA string in a detail view; truncate in cards with a title tooltip, do not rewrite
  • Localization: translate your chrome ("Body class"), not the NHTSA token, unless you maintain a reviewed map

Filters and SEO pages

BodyClass is tempting for landing pages: "/suv-vin-decode". Prefer stable, reviewed slugs mapped from a small allow-list of NHTSA strings, not free-text BodyClass in the URL. When vPIC returns a rare or updated label, fall back to a generic "passenger vehicle" facet rather than minting a new marketing page overnight.

For in-app filters, store the raw BodyClass value you received and compare with exact match or a maintained synonym table. Do not includes("sport") and call it a sport trim filter -- BodyClass parentheticals and English wording will false-positive.

Tests that lock honesty

import assert from "node:assert/strict";

assert.equal(viewBodyClass("  Sedan/Saloon ").label, "Sedan/Saloon");
assert.equal(viewBodyClass("Not Applicable").label, null);
assert.equal(
  bodyAndTrimRows({ BodyClass: "Coupe", Trim: null, Series: "" }).some(
    (r) => r.k === "Trim / series" && /Not provided/i.test(r.v),
  ),
  true,
);
Enter fullscreen mode Exit fullscreen mode

Add a lint-style unit test that fails if any helper returns a string containing "Package" or "Edition" unless that substring existed in the raw vPIC fields. Invented suffixes tend to sneak in through "nice" formatters.

Takeaway

BodyClass from vPIC is a body taxonomy field, not a trim decoder. Display it with clear sourcing, show Trim/Series only when the API provides them, and leave package names to stickers and manufacturer data. Your free VIN UI stays trustworthy when every chip maps to a real field -- or admits the field was empty.

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

Top comments (0)