Validating Input: Email, Phone Numbers and URLs
What regular expressions can and cannot check in user input: email addresses, phone numbers, URLs and postal codes, and when a parser or a confirmation step is the better answer.
What a regex can check
Input validation
checks that incoming data has the expected type, length, format and
range before an application uses it. Regular expressions handle one
part of that job:
format validation, checking
that a value has the right shape: four digits and two
letters, an @ with something on both sides, a plus sign
followed by digits.
A shape is not the truth. A pattern cannot tell whether a mailbox
exists, whether a phone number belongs to the person typing it, or
whether 2026-02-30 is a real day. So a regex is usually
the first, cheap layer of a check, not the whole check:
Two security rules from web application security shape everything below. First, describe what is allowed (an allowlist) instead of trying to list what is dangerous. Second, validation that counts happens on the server. Validation does not replace output encoding or parameterized queries; a valid-looking value can still be hostile in another context.
Match the whole value
The most common validation bug is a pattern that finds a valid
part of the input and accepts the rest along with it.
Searching is the default in most APIs: Python's
re.search(), JavaScript's test() and PHP's
preg_match() all succeed if the pattern matches
anywhere. Take a Dutch postal code, four digits (never starting with
0) and two letters, like 2511 CV:
import re
postcode = r"[1-9][0-9]{3} ?[A-Z]{2}"
values = ["2511 CV", "2511CV", "0123 AB", "2511 CV; DROP TABLE", "x2511 CV"]
for value in values:
found = re.search(postcode, value) is not None
whole = re.fullmatch(postcode, value) is not None
print(f"{value!r:23} search: {found!s:5} fullmatch: {whole}")
'2511 CV' search: True fullmatch: True '2511CV' search: True fullmatch: True '0123 AB' search: False fullmatch: False '2511 CV; DROP TABLE' search: True fullmatch: False 'x2511 CV' search: True fullmatch: False
re.fullmatch() succeeds only if the pattern covers the
entire string (a
full match). In other
languages you add the
anchors
^ and $ yourself, and there is a trap:
$ does not mean "end of string" everywhere.
The trailing-newline trap
In Python and PHP (PCRE), $ also matches just
before a newline at the very end of the string. A value
read from a file or sent by a script often ends with one. Run the
same anchored pattern in three languages:
import re
value = "2511 CV\n" # a trailing newline, e.g. from a file or an API
print(re.search(r"^[1-9][0-9]{3} ?[A-Z]{2}$", value) is not None)
print(re.search(r"^[1-9][0-9]{3} ?[A-Z]{2}\Z", value) is not None)
print(re.fullmatch(r"[1-9][0-9]{3} ?[A-Z]{2}", value) is not None)
True False False
const value = "2511 CV\n";
console.log(/^[1-9][0-9]{3} ?[A-Z]{2}$/.test(value));
console.log(/^[1-9][0-9]{3} ?[A-Z]{2}$/m.test(value));
false true
<?php
$value = "2511 CV\n";
var_dump(preg_match('/^[1-9][0-9]{3} ?[A-Z]{2}$/', $value));
var_dump(preg_match('/^[1-9][0-9]{3} ?[A-Z]{2}$/D', $value));
var_dump(preg_match('/^[1-9][0-9]{3} ?[A-Z]{2}\z/', $value));
int(1) int(0) int(0)
Python and PHP accept the value with its newline; JavaScript rejects
it (unless the m flag is on, which makes
$ match at every line end). The robust choices are
re.fullmatch() or \Z in Python (in Python,
\Z means the absolute end), and \z or the
D modifier in PHP. Never turn on multiline mode in a
validation pattern: it lets a valid first line carry anything on the
lines after it.
Postal codes: normalize, then check
People type the same value in many ways: in lowercase, with or without a space, with a stray space at the end. Rejecting all of those annoys users and buys no safety. A better approach is to normalize first (trim spaces, fix the case), then validate strictly, then store one canonical form. The format itself differs per country:
A Dutch postal code is four digits (never starting with 0) and
two letters, written as 2511 CV. People type it as
2511cv, 2511 CV or
2511 cv:
import re
POSTCODE = re.compile(r"([1-9][0-9]{3}) ?([A-Z]{2})")
def normalize_postcode(raw):
"""Return the postal code as '1234 AB', or None if it is not valid."""
value = raw.strip().upper()
if len(value) > 7: # cheap length check before any regex
return None
match = POSTCODE.fullmatch(value)
if match is None:
return None
return f"{match[1]} {match[2]}"
for raw in [" 2511cv ", "2511 CV", "2511 CV", "2511-CV", "251 CV", "٢٥١١ CV"]:
print(f"{raw!r:12} -> {normalize_postcode(raw)}")
' 2511cv ' -> 2511 CV '2511 CV' -> 2511 CV '2511 CV' -> None '2511-CV' -> None '251 CV' -> None '٢٥١١ CV' -> None
The pattern uses [0-9] on purpose. In Python 3,
\d matches any Unicode decimal digit, so
\d{4} would accept the Arabic-Indic digits in the
last line (see
character classes and shorthands). For validation, say exactly which characters you allow.
The same check works on a whole file on the command line. Here
grep -v inverts the match, so it prints the
customer records whose postal code is not in the
expected form, with line numbers (-n):
printf '%s\n' 'KE-0042;2511 CV' 'KE-0043;2511cv' 'KE-0044;0123 AB' \
'KE-0045;3011 AH' 'KE-0046;3011 AH ' 'KE-0047;9711LM' > customers.csv
# Lines whose postal code (field 2) is NOT in the form 1234 AB or 1234AB
grep -nvE ';[1-9][0-9]{3} ?[A-Z]{2}$' customers.csv
echo "---"
grep -cE ';[1-9][0-9]{3} ?[A-Z]{2}$' customers.csv
2:KE-0043;2511cv 3:KE-0044;0123 AB 5:KE-0046;3011 AH --- 3
Line 5 looks fine on screen. It fails because of an invisible trailing space, which is exactly the kind of thing normalization should remove before the strict check.
A US ZIP code is five digits, such as 20500. The
longer ZIP+4 form adds a hyphen and four digits that narrow the
area down to a street segment or a building (the +4 parts in
these examples are made up). ZIP codes in New England, New
Jersey, Puerto Rico and the US Virgin Islands start with 0, and
that leading zero is the classic bug: a spreadsheet or a numeric
database column quietly turns 02134 into
2134. Store ZIP codes as text.
import re
ZIP = re.compile(r"([0-9]{5})(?:-?([0-9]{4}))?")
def normalize_zip(raw):
"""Return '12345' or '12345-6789', or None if it is not valid."""
value = raw.strip()
if len(value) > 10: # cheap length check before any regex
return None
match = ZIP.fullmatch(value)
if match is None:
return None
return match[1] if match[2] is None else f"{match[1]}-{match[2]}"
for raw in [" 20500 ", "20500-0001", "205000001", "2134", "20500 0001", "٢٠٥٠٠"]:
print(f"{raw!r:13} -> {normalize_zip(raw)}")
' 20500 ' -> 20500 '20500-0001' -> 20500-0001 '205000001' -> 20500-0001 '2134' -> None '20500 0001' -> None '٢٠٥٠٠' -> None
The pattern uses [0-9] on purpose. In Python 3,
\d matches any Unicode decimal digit, so
\d{5} would accept the Arabic-Indic digits in the
last line (see
character classes and shorthands). And 2134 is rejected rather than "repaired": it
may be a ZIP code that lost its leading zero, but the code
cannot know that, so the user is asked again.
The same check works on a whole file on the command line. Here
grep -v inverts the match, so it prints the order
records whose ZIP code is not in the expected form,
with line numbers (-n):
printf '%s\n' 'A-1001;20500' 'A-1002;2134' 'A-1003;20500-0001' \
'A-1004;20500 ' 'A-1005;02134' 'A-1006;205000001' > orders.csv
# Lines whose ZIP code (field 2) is NOT in the form 12345 or 12345-6789
grep -nvE ';[0-9]{5}(-[0-9]{4})?$' orders.csv
echo "---"
grep -cE ';[0-9]{5}(-[0-9]{4})?$' orders.csv
2:A-1002;2134 4:A-1004;20500 6:A-1006;205000001 --- 3
Line 4 looks fine on screen. It fails because of an invisible trailing space, and line 6 is a ZIP+4 without its hyphen: exactly the kind of thing normalization should fix before the strict check.
A UK postcode has two parts. The
outward code (SW1A,
M1, B33) names the postal area and
district; the inward code (1AA,
1AE, 8TH) is always one digit and two
letters and narrows it down to a street or a few addresses.
Outward codes come in six shapes (A9, A99, AA9, AA99, A9A and
AA9A, where A is a letter and 9 a digit), so the length varies,
but the inward code is always the last three characters. That
makes normalization easy: drop every space, then put one back
before the last three. SW1A 1AA
is Buckingham Palace; the other values are format examples.
import re
# Outward code: area (1-2 letters) + district (a digit, then maybe a digit or
# letter). Inward code: always one digit and two letters, the last 3 characters.
POSTCODE = re.compile(r"([A-Z]{1,2}[0-9][A-Z0-9]?)([0-9][A-Z]{2})")
def normalize_postcode(raw):
"""Return the postcode as 'SW1A 1AA', or None if the shape is wrong."""
value = re.sub(r"\s+", "", raw).upper() # drop every space first
if value == "GIR0AA": # a historic exception
return "GIR 0AA"
if len(value) > 7: # cheap length check before any regex
return None
match = POSTCODE.fullmatch(value)
if match is None:
return None
return f"{match[1]} {match[2]}"
for raw in [" sw1a1aa ", "SW1A 1AA", "m11ae", "B338TH", "SW1A-1AA",
"SW1A 1A", "gir 0aa", "SW١A ١AA"]:
print(f"{raw!r:12} -> {normalize_postcode(raw)}")
' sw1a1aa ' -> SW1A 1AA 'SW1A 1AA' -> SW1A 1AA 'm11ae' -> M1 1AE 'B338TH' -> B33 8TH 'SW1A-1AA' -> None 'SW1A 1A' -> None 'gir 0aa' -> GIR 0AA 'SW١A ١AA' -> None
The pattern checks the shape and nothing more, and it uses
[0-9] on purpose: in Python 3, \d
matches any Unicode decimal digit, so it would accept the
Arabic-Indic digits in the last line (see
character classes and shorthands). A stricter regex can encode more of Royal Mail's rules (some
letters never appear in some positions), but only a lookup in
Royal Mail's Postcode Address File (PAF) tells you whether a
postcode exists, so real address forms look it up. One historic
postcode, GIR 0AA, does not follow the current
rules and is handled as an explicit exception. Long patterns
also hide anchoring bugs: a pattern published in UK government
guidance reads ^([Gg][Ii][Rr] 0[Aa]{2})|(…)$, so
^ anchors only the first alternative and
$ only the second, and with a search function a
normal postcode with text in front of it passes (see
match the whole value).
The same check works on a whole file on the command line. Here
grep -v inverts the match, so it prints the order
records whose postcode is not in the canonical form,
with line numbers (-n):
printf '%s\n' 'A-2001;SW1A 1AA' 'A-2002;sw1a 1aa' 'A-2003;SW1A1AA' \
'A-2004;M1 1AE ' 'A-2005;B33 8TH' 'A-2006;GIR 0AA' > orders.csv
# Lines whose postcode (field 2) is NOT in the form "outward inward"
grep -nvE ';[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][A-Z]{2}$' orders.csv
echo "---"
grep -cE ';[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][A-Z]{2}$' orders.csv
2:A-2002;sw1a 1aa 3:A-2003;SW1A1AA 4:A-2004;M1 1AE 6:A-2006;GIR 0AA --- 2
Line 4 looks fine on screen. It fails because of an invisible trailing space; lines 2 and 3 only need normalizing, and line 6 is the historic exception the shape pattern does not cover.
Email addresses
The formal grammar for email addresses (RFC 5322) allows quoted
local parts, comments and other things almost nobody uses. Even the
HTML standard refuses to follow it. For
<input type="email"> it defines its own, simpler
pattern and calls it "a willful violation of RFC 5322", because that
grammar is "simultaneously too strict (before the '@' character),
too vague (after the '@' character), and too lax" to be useful. Here
is that pattern, run in Node.js:
// The "valid email address" pattern from the HTML standard (type="email")
const htmlEmail = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const addresses = [
"amara.okafor@example.com",
"billing+invoices@example.com",
"a@b",
"a..b@example.com",
"amara okafor@example.com",
"jörg@example.com",
"amara@example..com",
];
for (const address of addresses) {
console.log(htmlEmail.test(address) ? "accept" : "reject", address);
}
accept amara.okafor@example.com accept billing+invoices@example.com accept a@b accept a..b@example.com reject amara okafor@example.com reject jörg@example.com reject amara@example..com
PHP's built-in email filter gives different answers for the same list:
<?php
$addresses = [
'amara.okafor@example.com',
'billing+invoices@example.com',
'a@b',
'a..b@example.com',
'amara okafor@example.com',
'jörg@example.com',
'amara@example..com',
];
foreach ($addresses as $address) {
$ok = filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
echo ($ok ? 'accept' : 'reject'), ' ', $address, "\n";
}
accept amara.okafor@example.com accept billing+invoices@example.com reject a@b reject a..b@example.com reject amara okafor@example.com reject jörg@example.com reject amara@example..com
Two respected validators disagree about a@b (no dot in
the domain) and a..b@example.com (two dots in a row).
Neither is "wrong"; they made different trade-offs. Both reject
jörg@example.com, although internationalized email
standards do allow non-ASCII characters before the @,
and some mail systems accept them. The lesson: no pattern is
the correct email regex, so do not spend a day looking for
one.
Confirm by sending
The only reliable test of an email address is to use it. A
confirmation email is a
message with a single-use link or code, sent to the address the user
entered; the address counts as verified only after the user proves
they received it. That catches typos (amara@exmaple.com
is perfectly well-formed), proves the mailbox exists, and proves it
belongs to this person. With that in place, the regex only has to
catch obvious mistakes, so keep it loose: something like "no spaces,
one @, something on both sides" is enough. A strict
pattern that rejects a real customer's address costs more than a
loose one that lets a typo through to the confirmation step.
Phone numbers
Phone numbers arrive as 06 12345678,
+31 6 1234 5678, 0031-6-12345678 or
with an extension. Inside the Netherlands a number starts with
the trunk prefix 0 (06 for mobiles); after the country code +31
that 0 is dropped.
Phone numbers arrive as (202) 555-0142,
202-555-0142, 1-202-555-0142,
+1 202 555 0142 or with an extension
(x12). US numbers belong to the North American
Numbering Plan (NANP, country code +1, shared with Canada and
much of the Caribbean): a three-digit area code and a
seven-digit local number, in the form NXX-NXX-XXXX, where N is a
digit from 2 to 9. The numbers on this page use the range
555-0100 to 555-0199, which is reserved for fiction and
advertising.
Phone numbers arrive as 020 7946 0018,
+44 20 7946 0018, +44 (0)20 7946 0018,
0044 20 7946 0018 or with an extension. Inside the
UK a number starts with the trunk prefix 0 (020 for London, 07
for most mobiles); after the country code +44 that 0 is dropped.
The numbers on this page come from ranges Ofcom reserves for TV
and radio drama, such as 020 7946 0000 to 0999 and 07700 900000
to 900999.
Trying to write one pattern that accepts every local habit is hopeless. Instead, store one standard form and convert input into it.
E.164: one format to store
E.164 is the ITU-T (International
Telecommunication Union, standardization sector) numbering plan for
international phone numbers. A number in E.164 form is a country
code followed by the national number, at most 15 digits in total,
usually written with a leading + and no spaces:
+31612345678, +12025550142 or
+442079460018. Its shape fits a short pattern:
\+[1-9][0-9]{1,14} (country codes never start with 0).
import re
E164 = re.compile(r"\+[1-9][0-9]{1,14}")
def to_e164(raw):
"""Very rough clean-up: remove spaces, ( ) . -, turn a leading 00 into +."""
digits = re.sub(r"[ ().-]", "", raw)
if digits.startswith("00"):
digits = "+" + digits[2:]
return digits if E164.fullmatch(digits) else None
for raw in ["+31 6 12345678", "0031-6-1234-5678", "06 12345678",
"+31 (0)6 12345678", "+31 6 1234 5678 ext. 12"]:
print(f"{raw!r:26} -> {to_e164(raw)}")
'+31 6 12345678' -> +31612345678 '0031-6-1234-5678' -> +31612345678 '06 12345678' -> None '+31 (0)6 12345678' -> +310612345678 '+31 6 1234 5678 ext. 12' -> None
Look at the fourth line. The common notation
(0) means "dial this 0 only inside the country",
and the naive clean-up kept it. The result
+310612345678 passes the E.164 pattern but is a
wrong number. And 06 12345678 is rejected only
because the code does not know which country the user is in.
Both are problems of meaning that a pattern cannot
solve: you need the numbering rules of each country.
import re
E164 = re.compile(r"\+[1-9][0-9]{1,14}")
NANP = re.compile(r"\+1[2-9][0-9]{2}[2-9][0-9]{6}")
def to_e164(raw):
"""Very rough clean-up: remove spaces, ( ) . -, turn a leading 011 into +."""
digits = re.sub(r"[ ().-]", "", raw)
if digits.startswith("011"):
digits = "+" + digits[3:]
return digits if E164.fullmatch(digits) else None
for raw in ["+1 (202) 555-0142", "011 1 202 555 0142", "(202) 555-0142",
"1-202-555-0142", "+1 202 055 0142", "+1 202-555-0142 x12"]:
number = to_e164(raw)
nanp = number is not None and NANP.fullmatch(number) is not None
print(f"{raw!r:23} -> {number} NANP shape: {nanp}")
'+1 (202) 555-0142' -> +12025550142 NANP shape: True '011 1 202 555 0142' -> +12025550142 NANP shape: True '(202) 555-0142' -> None NANP shape: False '1-202-555-0142' -> None NANP shape: False '+1 202 055 0142' -> +12020550142 NANP shape: False '+1 202-555-0142 x12' -> None NANP shape: False
Look at the fifth line. +12020550142 passes the
E.164 pattern, but an exchange code never starts with 0 or 1, so
it cannot be a NANP number; the second pattern catches that. The
third and fourth lines are rejected only because the code does
not know what the user meant: (202) 555-0142
has no country code, and the leading 1 in
1-202-555-0142 is the long-distance prefix, which
happens to be the same digit as the country code. Both are
problems of meaning that a pattern cannot solve: you
need the numbering rules of each country. (From the US, 011 is
the prefix for international calls, where most of Europe uses
00.)
import re
E164 = re.compile(r"\+[1-9][0-9]{1,14}")
def to_e164(raw):
"""Very rough clean-up: remove spaces, ( ) . -, turn a leading 00 into +."""
digits = re.sub(r"[ ().-]", "", raw)
if digits.startswith("00"):
digits = "+" + digits[2:]
return digits if E164.fullmatch(digits) else None
for raw in ["+44 20 7946 0018", "0044-20-7946-0018", "020 7946 0018",
"+44 (0)20 7946 0018", "+44 7700 900123 ext. 12"]:
print(f"{raw!r:26} -> {to_e164(raw)}")
'+44 20 7946 0018' -> +442079460018 '0044-20-7946-0018' -> +442079460018 '020 7946 0018' -> None '+44 (0)20 7946 0018' -> +4402079460018 '+44 7700 900123 ext. 12' -> None
Look at the fourth line. The notation (0), common
on British business cards, means "dial this 0 only inside the
country", and the naive clean-up kept it. The result
+4402079460018 passes the E.164 pattern but is a
wrong number. And 020 7946 0018 is rejected only
because the code does not know which country the user is in.
Both are problems of meaning that a pattern cannot
solve: you need the numbering rules of each country.
For real applications, use a dedicated library (Google's open-source libphonenumber and its ports are the usual choice) to parse, validate and format numbers, and confirm ownership with a one-time code by SMS when it matters.
URLs: use a parser
URLs look easy to match and are easy to get dangerously wrong.
Imagine a login page that only redirects to your own site, checked
with ^https://example\.com. The pattern checks that the
text starts with those characters, not that the host
is example.com.
A URL parser is code that splits a
URL into its parts (scheme, user information, host, port, path,
query and fragment) by the rules of a URL standard, the same way a
browser or HTTP client will. Validate those parts instead of the raw
text. In JavaScript the parser is the built-in
URL class, which follows the WHATWG URL Standard that
browsers use:
const naive = /^https:\/\/example\.com/;
const allowedHosts = new Set(["example.com", "www.example.com"]);
function isAllowed(text) {
if (!URL.canParse(text)) return false;
const url = new URL(text);
return url.protocol === "https:" && allowedHosts.has(url.hostname);
}
for (const text of [
"https://example.com/account",
"https://EXAMPLE.com:443/account",
"https://example.com.example.net/login",
"https://example.com@example.org/",
"http://example.com/",
"not a url",
]) {
console.log(naive.test(text), isAllowed(text), text);
}
true true https://example.com/account false true https://EXAMPLE.com:443/account true false https://example.com.example.net/login true false https://example.com@example.org/ false false http://example.com/ false false not a url
The regex gets three of six wrong. It accepts a different host,
example.com.example.net, and a URL whose host is really
example.org: everything before the @ is
user information, not the host. It also rejects a perfectly good URL
written in capitals. The parser normalizes the host to lowercase and
drops the default port, and the allowlist then compares exact
values. That pattern (parse, then compare parts with an allowlist)
is how open redirects and server-side request forgery checks should
be written.
Python's urllib.parse.urlsplit() does the splitting. It
is lenient: it rarely raises an error, so check that the parts you
need are present.
from urllib.parse import urlsplit
for text in ["https://example.com:8443/meter/KE-0042?view=day#chart",
"https://example.com@example.org/",
"not a url"]:
parts = urlsplit(text)
print(repr(parts.scheme), repr(parts.hostname), parts.port, repr(parts.path))
'https' 'example.com' 8443 '/meter/KE-0042' 'https' 'example.org' None '/' '' None None 'not a url'
See URL parts for what each component means. A regex is still fine for finding URL-like text in a log or a chat message; just do not let it make security decisions.
Dates, IP addresses and other values with meaning
The same rule applies to any value with internal rules. A date pattern can check that the month is 01 to 12, but not that February has no 30th; a dotted-quad pattern can check four groups of digits, but writing one that only allows 0 to 255 makes it long and still does not know about leading zeros. Let the regex check the format if you like, then hand the value to a parser:
<?php
// The format is right in both cases; only a real check knows the meaning.
$date = '2026-02-30';
$looksRight = preg_match('/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/', $date);
[$y, $m, $d] = array_map('intval', explode('-', $date));
var_dump($looksRight, checkdate($m, $d, $y));
$ip = '192.0.2.256';
var_dump(preg_match('/^\d{1,3}(\.\d{1,3}){3}$/', $ip));
var_dump(filter_var($ip, FILTER_VALIDATE_IP));
int(1) bool(false) int(1) bool(false)
In Python the equivalents are
datetime.date.fromisoformat() and
ipaddress.ip_address(), both of which raise
ValueError for impossible values. More date and address
patterns are on
practical patterns: logs, IP addresses and dates; the rules for the values themselves are in
IPv4 addressing
and
dates and times in data formats.
Where validation happens
In the browser, the HTML pattern attribute gives
instant feedback (see
built-in form validation). The browser wraps the pattern as ^(?:…)$
and compiles it as a JavaScript regex with the v flag,
so you write it without anchors and it must match the whole value:
<input name="postcode" pattern="[1-9][0-9]{3} ?[A-Za-z]{2}" required>
Anyone can switch that check off, edit the page or send the request without a browser at all. Treat browser validation as a usability feature and validate again on the server, with the same rules. Practical server-side habits:
- Limit the length first. A length check is instant and protects every later step, including the regex itself (see performance and catastrophic backtracking).
- Normalize, then validate, then store the canonical form, so the rest of the system only sees one spelling.
-
Allowlist explicit characters:
[0-9]and[A-Z]rather than\d,\wor., whose meaning differs per flavor and per Unicode setting. -
Fail closed. If the regex engine reports an error
(PHP's
preg_match()returnsfalse), treat the input as invalid, not as valid. - Give a helpful message that shows the expected form ("four digits and two letters, like 2511 CV"), not "invalid input".