Compare resume and job description: by hand or in code
Published
Compare resume and job description: a field-by-field method
To compare a resume and a job description, split the posting into requirements, find the part of the CV that answers each one, and mark it met, missing or unclear. That is the whole method, whether you are a candidate checking one application or a developer matching thousands of applicants to roles. The difference is only who does the finding. By hand it takes a highlighter and ten minutes. In a product it takes a parsed CV, a parsed posting and a function you can read.
This article covers both. The first half is the manual comparison, done properly. The second half is for the developer building resume and job description comparison into a job board, an applicant tracking system or a candidate-matching tool. We build ResumeJSON, a parsing API that turns a CV into typed JSON, so read the parsing parts as written by an interested party. The comparison itself is yours to own, whichever parser you use.
Why a keyword count is the wrong comparison
The most common way to compare a resume with a job description is to count shared words. Paste both texts into a tool, get a percentage back. It is quick, and it misleads in three predictable ways.
- It cannot do arithmetic. "5+ years of backend experience" is a sum over job dates. A word counter sees "5" and "years" and has no idea whether the CV adds up to two years or eleven.
- It rewards repetition. A CV that says "Python" nine times scores higher than one that lists it once beside a project that proves it.
- It treats every word as equal. A posting's hard requirement (a licence, a work permit, a degree) and its nice-to-have ("familiarity with Figma") weigh the same.
A useful comparison works on requirements, not on words. Each requirement points at a specific part of the CV, and each gets its own answer.
Step 1: turn the job description into requirements
Read the posting and write every requirement on its own line. Then sort each line into one of these groups, because each group is checked against a different part of the CV:
| Requirement type | Example from a posting | Where the CV answers it |
|---|---|---|
| Hard requirement | "Registered nurse licence" | Certifications |
| Experience floor | "At least 4 years in data engineering" | Job titles and their dates |
| Must-have skill | "Strong SQL" | Skills list, and the roles that used it |
| Nice-to-have skill | "Exposure to Airflow" | Skills list |
| Education | "Degree in computer science or similar" | Education |
| Language | "Fluent German" | Languages |
| Logistics | "Based in Lisbon or willing to relocate" | Location |
Leave out anything a CV cannot answer as data. "Team player", "passionate" and "strong communicator" are real preferences, and they belong to the interview. Putting them in the comparison only adds noise.
Step 2: find the evidence in the resume
Go down your list and, for each requirement, find the line of the CV that answers it. Mark it one of three ways:
- Met. The CV states it plainly. A certification with the right name, a skill in the list, dates that add up.
- Missing. The CV does not mention it anywhere.
- Unclear. The CV hints at it without stating it. A role titled "Data Analyst" that may or may not have involved SQL, or a job with no end date.
"Unclear" is the most useful mark on the page. For a candidate it says what to make explicit before applying. For a recruiter it says what to ask on the first call. Collapsing it into "missing" is how good applicants get filtered out for the way they formatted a document.
For experience floors, do the sum yourself. List each relevant role with its start and end month, remove overlaps, and add up the months. Two part-time roles held at the same time count once.
Step 3: decide what the gaps mean
With every requirement marked, the comparison reads itself:
- Every hard requirement met, most must-haves met: a strong fit. Apply, or move the candidate forward.
- A hard requirement missing: usually a stop, unless the posting says it can be earned on the job.
- Several items unclear: the CV is the problem, not the person. Rewrite it, or ask.
If you are the candidate
Rewrite the unclear items so they are stated, never invented. If you used SQL daily as a data analyst, put SQL in your skills and say what you did with it in that role. Use the posting's own wording where it is true for you, because both people and software look for it. Do not paste the job description into white text or stuff keywords into a footer: a recruiter reading the parsed record sees it, and it reads as a trick.
To see what software will extract from your CV before a recruiter does, run it through our free resume parser. It needs no signup and does not store the document, and it shows you the name, roles, dates, skills and languages a machine reads out of your file. If a role's dates come back empty, or your skills list is shorter than you thought, that is the gap to fix before you compare anything.
When the manual way is enough
For a single application, the method above is the right tool. It takes ten or fifteen minutes, and you learn things about your own CV that no score would tell you. The same goes for a hiring manager with a handful of applicants for one role: read each CV against the requirement list, mark it, done.
Software starts to earn its place when the comparison repeats: dozens of applicants a week, many open roles, or a product that promises its users matching as a feature. At that point the steps stay the same and the code does the finding.
Building resume and job description comparison into a product
If you run a job board or an ATS, "compare this applicant to this job" is the same three steps, run on structured data. The work is in getting both sides into a shape a function can read.
Get the resume into fields
A comparison needs the CV as a record, not a PDF. The minimum set of fields:
| Field | Used for |
|---|---|
skills[] | Must-have and nice-to-have skills |
work[] with title, start_date, end_date, is_current | Experience floors, relevant titles |
total_years_experience | A quick experience check |
education[] with degree, field_of_study | Education requirements |
certifications[] with name | Hard requirements |
languages[] with language, proficiency | Language requirements |
basics.location | Logistics |
You can build this yourself. We have walked the do-it-yourself path in Python, Node.js and with ChatGPT, and pulling skills out specifically is covered in extract skills from a resume. Or call a parsing API and get the record back directly. ResumeJSON returns exactly the fields above for PDF, DOCX, plain text and images of a page, with dates as YYYY-MM or YYYY and missing values as null rather than a guess.
Get the job description into fields
Most job boards already hold part of the posting as structure: location, seniority, maybe a skills tag list. Store requirements explicitly when the employer creates the job, rather than parsing free text afterwards:
type Requirement =
| { kind: 'certification'; name: string; hard: true }
| { kind: 'years'; min: number; hard: boolean }
| { kind: 'skill'; name: string; hard: boolean }
| { kind: 'language'; language: string; hard: boolean }
| { kind: 'location'; city: string; hard: boolean };A form with a "requirements" section, each line tagged required or preferred, costs the employer a minute and saves every comparison from guessing which sentence of the prose mattered.
Compare, with reasons
The comparison is a plain function. It returns the three marks from the manual method, plus the reason for each:
type Mark = 'met' | 'missing' | 'unclear';
function compare(r: Resume, reqs: Requirement[]) {
return reqs.map(req => {
switch (req.kind) {
case 'skill': {
const has = r.skills.some(s => s.toLowerCase() === req.name.toLowerCase());
return { req, mark: (has ? 'met' : 'missing') as Mark };
}
case 'years': {
const y = r.total_years_experience;
if (y === null) return { req, mark: 'unclear' as Mark, why: 'dates could not be read' };
return { req, mark: (y >= req.min ? 'met' : 'missing') as Mark, why: `${y} years listed` };
}
case 'certification': {
const has = r.certifications.some(c => c.name?.toLowerCase().includes(req.name.toLowerCase()));
return { req, mark: (has ? 'met' : 'missing') as Mark };
}
case 'language': {
const has = r.languages.some(l => l.language.toLowerCase() === req.language.toLowerCase());
return { req, mark: (has ? 'met' : 'missing') as Mark };
}
case 'location': {
const loc = r.basics.location;
if (loc === null) return { req, mark: 'unclear' as Mark, why: 'no location given' };
return { req, mark: (loc.toLowerCase().includes(req.city.toLowerCase()) ? 'met' : 'unclear') as Mark };
}
}
});
}Three choices in there are deliberate:
- A
nullbecomes "unclear", never "missing". A CV with no dates is a formatting question, not proof of zero experience. - Skill matching is exact after lowercasing. Add a synonyms table ("Postgres" and "PostgreSQL") as you find misses, in your own code where you can read it.
- The output is a list of marks, not one score. Show it to the recruiter as a checklist. If you must sort applicants, sort by hard requirements met first, then must-haves, and keep the checklist one click away.
What to do with the result (route to review, never auto-reject, check the screen against real hiring outcomes) is covered step by step in automated resume screening. For running a whole backlog of CVs through the parse first, see bulk resume parsing.
Why not ask a model for a match score?
It is tempting to send both documents to a language model and ask for a percentage. You get a number quickly. You also get a number that changes when you run it twice, that nobody can explain to a rejected candidate, and that quietly weighs things you never asked it to. Use a model to read the CV into fields if you like; that is extraction, and it can be checked. Keep the decision in code.
Where ResumeJSON fits
ResumeJSON does the parsing step and nothing else. It does not score, rank or compare, because that logic belongs in your product. It gives you the resume side of the comparison as one typed schema for every CV, in about two seconds.
As of September 2026 the API is sold through RapidAPI. The Basic plan is $0 for 100 parses a month with a hard cap, Pro is pay per use at $0.05 a parse, and paid plans start at $29 a month for 1,000 parses. The docs carry the current table and the full field reference, and the free parser lets you try it on a real CV first.
Whichever side you are on, the method is the same: requirements on one side, evidence on the other, and an honest "unclear" where the CV does not say.