Form handling, serialization and validation

Read every input type correctly, serialise a form including files, intercept submission, and get a clear picture of what the Validate plugin adds.

Reading every kind of control

// Text, textarea, select, number, date — all one accessor
$('#email').val();

// Checkboxes and radios: read the group, not the individual input
$('input[name="notify"]:checked').map((_, el) => el.value).get();   // ['email', 'sms']
$('#terms').prop('checked');                                       // boolean

// A single checkbox with no value attribute reports 'on' — always set value in markup
console.log($('#newsletter').val());   // 'on' unless value="yes" is present

// Multi-select returns an array
$('#tags').val();                      // ['red', 'blue']

// File inputs: .val() returns a fake path; use the DOM property for real Files
const files = $('#attachment')[0].files;      // FileList
const first = files[0] ?? null;
console.log(first?.name, first?.size, first?.type);

// Files can only be appended to FormData; they cannot be serialised
const formData = new FormData($('#upload')[0]);
formData.append('attachment', first);
ControlRead withReturns
input[type=text].val()String
input[type=number].val() then parseString — never a number
input[type=checkbox] (group):checked + mapArray of values
input[type=radio]:checkedZero or one value
select.val()String, or an array when multiple
input[type=file][0].filesFileList
input[type=range].val()String — parse it
⚠️
.val() always returns a string or an array of strings. Summing form values without a parse is the classic bug: '1' + '2' is '12', and a validation check like value > 100 compares a string against a number. Parse at the boundary and validate the parsed value.

Serialization

// serialize() -> a query string. Only successful controls are included.
$('#signup').serialize();
// "email=a%40b.test&plan=team&notify=email&notify=sms"

// serializeArray() -> an array of name/value pairs. Better when names repeat.
$('#signup').serializeArray();
// [{ name: 'email', value: 'a@b.test' }, { name: 'notify', value: 'email' }, ...]

// Turning that array into an object loses repeated names — handle them deliberately.
function toObject(pairs) {
  return pairs.reduce((acc, { name, value }) => {
    if (name in acc) {
      acc[name] = [].concat(acc[name], value);      // collect repeats into an array
    } else {
      acc[name] = value;
    }
    return acc;
  }, {});
}
console.log(toObject($('#signup').serializeArray()));

// What is excluded: disabled controls, unchecked boxes, inputs with no name,
// and file inputs. Submit buttons are excluded unless they were the trigger.
// A <fieldset disabled> excludes everything inside it.

// Files must go through FormData instead:
const fd = new FormData(document.getElementById('signup'));
fetch('/api/signup', { method: 'POST', body: fd })
  .then((res) => res.json())
  .then(console.log);
  • Unchecked checkboxes are simply absent, not false. The server must interpret "missing" as false; do not require a default in the form.
  • A control with a duplicate name serialises twice, which is correct for a checkbox group and usually wrong for a text input.
  • Serialisation uses encodeURIComponent semantics, so values with newlines and ampersands survive the round trip.
  • FormData from a form element is the modern equivalent and handles files, which the jQuery helpers never did.

Submission, validation and the plugin

// Intercepting submission: always preventDefault, then decide.
$('#signup').on('submit', function (event) {
  event.preventDefault();
  const $form = $(this);

  // The browser's own checks first — free, and it focuses the first problem.
  if (!$form[0].checkValidity()) {
    $form[0].reportValidity();
    return;
  }

  const payload = toObject($form.serializeArray());
  delete payload.passwordConfirm;                       // never send the confirmation

  $form.find(':submit').prop('disabled', true);         // stop a double submit

  fetch($form.attr('action'), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  })
    .then(async (res) => {
      if (res.status === 422) {
        const { errors } = await res.json();            // { email: 'Already registered' }
        $form.find('.field-error').text('');            // clear previous
        Object.entries(errors).forEach(([field, message]) => {
          $form.find(`[name="${field}"]`).addClass('is-invalid')
               .attr('aria-invalid', 'true')
               .siblings('.field-error').text(message);
        });
        $form.find('[aria-invalid="true"]').first().trigger('focus');
        return;
      }
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      window.location.assign('/welcome');
    })
    .catch((error) => $form.find('.form-error-summary').text(error.message).show())
    .finally(() => $form.find(':submit').prop('disabled', false));
});
jQuery Validate featureWhat it gives youCost
rules optionDeclarative per-field constraintsA second source of truth beside server rules
messages optionCustom error textI18n needs a message catalogue
remote ruleAsync check such as a taken usernameOne request per keystroke unless debounced
Custom methods$.validator.addMethodYou are maintaining a validation DSL
valid() / element()Programmatic checksUseful when the submit is triggered by a wizard
Unobtrusive integrationReads HTML5 attributesActively discouraged in jQuery Validate's current documentation
NothingThe Constraint Validation APIFree, no dependency, already in every browser

The honest position on jQuery Validate: it earns its place only when the form is complex enough to need cross-field rules in a DSL and the project is already committed to jQuery. For a straightforward signup form, the Constraint Validation API plus server-side validation is less code and less maintenance.

FAQ

Why are my unchecked checkboxes missing from the data?
That is how HTML form submission has always worked: an unchecked checkbox is not a successful control, so it is absent. Either treat absence as false on the server, or add a hidden input with the same name and a default value before the checkbox.
How do I validate before enabling the submit button?
Listen to input and change on the form, run form.checkValidity(), and toggle the button's disabled property. Debounce if the form is large, and make sure the button's disabled state is not the only place the reason is communicated.

Attributes, properties and the data cache Deferreds, promises and async patterns

Last refreshed 2026-09-18.