ResumeJSON

Compare resume and job description: by hand or in code

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.

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 typeExample from a postingWhere 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:

  1. Met. The CV states it plainly. A certification with the right name, a skill in the list, dates that add up.
  2. Missing. The CV does not mention it anywhere.
  3. 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:

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:

FieldUsed for
skills[]Must-have and nice-to-have skills
work[] with title, start_date, end_date, is_currentExperience floors, relevant titles
total_years_experienceA quick experience check
education[] with degree, field_of_studyEducation requirements
certifications[] with nameHard requirements
languages[] with language, proficiencyLanguage requirements
basics.locationLogistics

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:

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.

All articles