Skip to content

Extracting Fields From a Job Application Form

8 min read · updated August 11, 2026

A form looks like the easy case: the fields are named, the layout is fixed, the answers are short. The difficulty is entirely in the negative space — what an empty box means, whether a tick is a tick, and which question a value belongs to when the two are separated by an inch of white paper.

Blank, N/A and absent are three states

A schema field that is either a string or null can represent two outcomes. A paper or scanned form produces at least four, and collapsing them loses information that somebody downstream needs:

  • Answered. The field is present and has a value.
  • Explicitly not applicable. The applicant wrote N/A, n/a, a dash, or struck the field through. This is an answer. It says the applicant read the question and it does not apply to them.
  • Left blank. The field exists on the form and is empty. This is not an answer, and depending on the field it may make the application incomplete, or it may be a deliberate refusal.
  • Not on this form. The question does not appear at all, because the form is an older revision or a different jurisdiction’s version. This is a fact about the document, not about the applicant.

Two of those look identical in a schema with only null. The consequence is real: a completeness check cannot distinguish an applicant who declined to answer from a form that never asked, and a reviewer chasing a missing answer contacts someone who has nothing to add. Model it with a status enum on every field — answered, not_applicable, blank, field_absent — and put the value in a separate property that is only meaningful when the status is answered. The downstream half of that decision, what a pipeline does once a required field comes back empty, is missing required field handling.

Instruct the model explicitly, because the default behaviour runs against you. Asked to extract a form, a model will normalise N/A to null without being asked, on the entirely reasonable assumption that you wanted clean data. It is exactly the same class of helpfulness that turns “March 2019” into a specific day.

A checkbox is not text

Checkbox extraction is a spatial task wearing a textual disguise, and it fails in ways that text extraction does not. The state is carried by marks that are not characters: an X, a tick, a filled circle, a diagonal stroke, a scribble that covers two boxes, or a printed box that was already dark. There is no text layer for any of it, so a PDF-text pipeline returns the labels with no indication of which was selected — and the labels alone read as a perfectly sensible list.

Four specific failures are worth designing against:

  • Nothing is selected. A single-choice group with no mark is a valid observation and a model asked “which option was selected” will pick the most plausible one. The prompt has to permit none and the schema has to accept it.
  • More than one is selected in a group that allows one. This is usually a correction the applicant did not strike through cleanly, and it is a review case, not a value.
  • The mark is between boxes. On a dense grid the nearest-box rule and the containing-box rule disagree. Record the option and a flag rather than resolving it silently.
  • The form is an AcroForm. If the PDF has real form fields, the checkbox state is in the file and needs no vision at all — and it is authoritative in a way a picture of a tick is not. Check for form fields before rendering anything; PDF parsing covers what is actually in the file.

The AcroForm check is the highest-value line in the whole pipeline for digitally-submitted applications, and it is routinely skipped because the pipeline was designed for scans. A flattened, printed and rescanned PDF loses those fields; a directly-submitted one keeps them.

Joining a value to its label

On a form the label and the value are separate objects on the page, and the relationship between them is layout. Three arrangements dominate, and a heuristic tuned for one breaks the others: the label to the left with the value on the same baseline, the label above with the value below, and the label inside a box with the value in the adjacent cell of a table.

Two-column forms make this actively dangerous rather than merely fiddly. A text extractor emitting in drawing order can interleave the columns, so a label from the left column lands next to a value from the right — and the result is a well-formed record with the wrong values in the right fields. That failure has no textual signature at all. The defence is to work from coordinates: cluster text runs into columns by x-position first, and only then read within each column, as in two-column PDF reading order.

Where a form is genuinely fixed — the same revision, the same printer, every time — a template with regions per field is more reliable and far cheaper than a model call. The condition is that you detect the revision and refuse to apply the template when it does not match, because a template applied to a form that shifted by half an inch produces confident nonsense. Anchor on something invariant, such as a printed form number in the footer, and treat an unrecognised revision as a routing decision rather than an error.

The section that must not join the rest

Job application forms in several jurisdictions carry a voluntary self-identification section covering characteristics such as ethnicity, sex, disability and veteran status. In the US these are collected under equal-opportunity monitoring obligations, and the whole point of the arrangement is that the data is used for aggregate reporting and kept away from the hiring decision.

That has a concrete engineering consequence. If your extraction emits one flat record, the monitoring data is now sitting in the same object as the qualifications, and anything reading that object — a ranking model, a screening rule, a reviewer’s screen — has access to it. Partition it in the pipeline, not in a policy document: extract the monitoring section into a separate record with its own identifier and its own store, and make the joining key something the hiring path does not hold.

Some jurisdictions additionally prohibit asking about salary history or criminal record at the application stage. If a form contains such a field — because it is an old revision, or from a different jurisdiction — the right behaviour is to detect and flag it, not to extract it into the same record as everything else. Decisions about what may lawfully be collected are for your counsel; what this page can say is that a schema which cannot represent “this field exists and was deliberately not extracted” will make that decision impossible to implement.

An application form is dense personal data and frequently includes identifiers that do not need to leave your infrastructure at all. Where a field is not needed for the extraction task, redact before the page reaches a third-party model rather than after — PII redaction covers the mechanics, and doing it upstream also keeps the identifiers out of request logs.

A schema with room for nothing

{
  "form_revision": "AF-14 (rev. 2023-06)",
  "fields": {
    "family_name":     { "status": "answered",       "value": "Okonkwo" },
    "middle_names":    { "status": "not_applicable", "value": null,
                         "evidence": "N/A" },
    "phone_secondary": { "status": "blank",          "value": null },
    "salary_expected": { "status": "field_absent",   "value": null }
  },
  "choices": {
    "work_authorisation": { "selected": ["yes"], "flags": [] },
    "shift_preference":   { "selected": [], "flags": ["no_option_marked"] },
    "notice_period":      { "selected": ["1_month", "3_months"],
                            "flags": ["multiple_marked_single_choice"] }
  },
  "monitoring_record_id": "sep-store://synthetic-0000",
  "review_required": true
}

Note what the record does not contain: the monitoring answers. It holds a reference into a separate store, so the object that flows into the hiring path cannot leak them by accident. review_required is derived rather than extracted — set it from the presence of any flag, any required field with status blank, and any unrecognised form revision. That gives you a review queue driven by the specific things that went wrong on this document rather than by a sampling rate.