VIN Validation RegEx

Dan is correct - VINs have a checksum. You can't utilize that in regex, so the best you can do with regex is casting too wide of a net. By that I mean that your regex will accept all valid VINs, and also around a trillion (rough estimate) non-VIN 17-character strings.

If you are working in a language with named capture groups, you can extract that data as well.

So, if your goal is:

  • Only to not reject valid VINs (letting in invalid ones is ok) then use Fransisco's answer, [A-HJ-NPR-Z0-9]{17}.

  • Not reject valid VINs, and grab info like model year, plant code, etc, then use this (note, you must use a language that can support named capture groups - off the top of my head: Perl, Python, Elixir, almost certainly others but not JS): /^(?<wmi>[A-HJ-NPR-Z\d]{3})(?<vds>[A-HJ-NPR-Z\d]{5})(?<check>[\dX])(?<vis>(?<year>[A-HJ-NPR-Z\d])(?<plant>[A-HJ-NPR-Z\d])(?<seq>[A-HJ-NPR-Z\d]{6}))$/ where the names are defined at the end of this answer.

  • Not reject valid VINs, and prevent some but not all invalid VINs, you can get specific like Pedro does.

  • Only accept valid VINs: you need to write code (just kidding, GitHub exists).

Capture group name key:

  • wmi - World manufacturer identifier
  • vds - Vehicle descriptor section
  • check - Check digit
  • vis - Vehicle identifier section
  • year - Model year
  • plant - Plant code
  • seq - Production sequence number

I can't help you with a perfect regex for VIN numbers -- but I can explain why this one is failing in your example of 1ftfw1et4bfc45903:

^[A-HJ-NPR-Za-hj-npr-z\d]{8}[\dX][A-HJ-NPR-Za-hj-npr-z\d]{2}\d{6}$

Explanation:

  • ^[A-HJ-NPR-Za-hj-npr-z\d]{8}
    This allows for 8 characters, composed of any digits and any letters except I, O, and Q; it properly finds the first 8 characters:
    1ftfw1et
  • [\dX]
    This allows for 1 character, either a digit or a capital X; it properly finds the next character:
    4
  • [A-HJ-NPR-Za-hj-npr-z\d]{2}
    This allows for 2 characters, composed of any digits and any letters except I, O, and Q; it properly finds the next 2 characters:
    bf
  • \d{6}$
    This allows for exactly 6 digits, and is the reason the regex fails; because the final 6 characters are not all digits:
    c45903