DEV Community

Vin Lookup
Vin Lookup

Posted on

Handling Null and Empty NHTSA vPIC Fields Without Inventing Specs

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),
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 null for unknowns, not empty strings, so clients cannot confuse "loaded empty" with "forgot to map the key."
  • Include source: "nhtsa-vpic" and fetchedAt.
  • 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:

  1. Rich consumer vehicle (most fields set).
  2. Sparse row (Make/Year only).
  3. 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)