Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Resume, Portfolio & GitHub Profile  Module 11 – Review, measure and automate

Resume as code: JSON Resume, Typst and CI

Keep your resume as JSON Resume data, check it against the schema, build a tagged PDF with Typst and let a CI workflow check every change.

  • Advanced
  • 25 minutes
  • Examples run with Python 3.14.8, Node.js 24.21.0, Pyodide 314.0.7, Typst 0.15.1, poppler 26.09.0, pdf.js 6.3.289, @jsonresume/schema 1.3.1, actionlint 1.7.12, shellcheck 0.11.0 and zizmor 1.30.1
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: One data file, a template and checks make every copy · The schema checks the shape of your data, not its truth. With a pass: 3 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Store a resume as JSON Resume data and build every copy from that one file
  • Validate the data against the schema, and catch what the schema lets through
  • Render a tagged, titled PDF/UA-1 resume from the data with Typst
  • Check, build and test every change with a pinned, read-only CI workflow
  • Generate tailored variants from one source without publishing private details

Before you start

What you’ll learn · Before you start

What you will learn

  • Store a resume as JSON Resume data and build every copy from that one file
  • Validate the data against the schema, and catch what the schema lets through
  • Render a tagged, titled PDF/UA-1 resume from the data with Typst
  • Check, build and test every change with a pinned, read-only CI workflow
  • Generate tailored variants from one source without publishing private details

Before you start

On this page

Asha kept her resume in four files: a Word document, a PDF, a LaTeX copy and her portfolio page. After one update two of them disagreed on a date, and the PDF she sent had no title. Resume as code keeps the facts in one data file and builds every copy from it with programs, so a change is made once and checked every time.

This lesson stores Asha’s resume as JSON Resume data, checks it against the schema, and shows what that check cannot see. You will write the two checks that close the gap, and see how Typst and a CI workflow turn the data into a PDF on every push.

One data file, a template and checks make every copy

Resume as code splits a resume into three parts that change for different reasons. The data says what is true: the names, the dates, the lines of work. A template says how it looks. Checks say what every copy must get right. A date changed once in the data changes in every copy built from it, and a new template changes the look and nothing else.

JSON Resume: An open format for a resume as one JSON file, with sections such as basics, work, education, skills and projects, defined by a JSON Schema that the project publishes under the MIT licence. is an open format for that data: one JSON file with sections such as basics, work, education, skills and projects. Its sections and types are written as a JSON Schema: A JSON document that describes what another JSON document may hold: the type of each value, the keys it may have and the patterns its strings must match, so that software can check a document against it., which the project publishes under the MIT licence. Typst: A typesetting system with its own markup and scripting language. Its compiler turns a .typ file, and any data that file reads, into a PDF, and it writes Tagged PDF by default. is a typesetting system whose compiler reads a template and the JSON file and writes a PDF. It writes tagged PDF: A PDF that stores the structure of its content, such as headings, paragraphs and lists, so that screen readers and other software can follow it in order. More in “LaTeX resume builds: TeX Live, variants and CI” by default, so screen readers and text extractors can follow the page in order. continuous integration: Running a project's tests automatically on every change pushed to its repository, usually with a workflow file, so that a change that breaks them shows at once. More in “Making a repository easy to review” runs those steps on a server on every push. On GitHub, a workflow file in the repository lists the steps, and the run stops at the first one that fails, so a broken resume never becomes a PDF. Step through one push:

Interactive · stage by stage One push through the resume pipeline Step through one change to Asha's resume. Each step reads what the step before it made, and the first step that fails stops the run.
One push through the resume pipeline, step 6 of 6. Showing resume.json, Schema check, Typst build, PDF checks, Keep the PDF. In focus: Keep the PDF.a step failsresume.jsonfacts only, no layoutSchema checktypes, dates, formatsTypst buildtemplate + data = PDFPDF checkstitle, tags, one pageKeep the PDFartifact or public site

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One file of facts. Asha changes one line of resume.json, the file that holds her name, jobs, projects, skills and education as data. Nothing in it says how a page looks.
  2. The schema check. The schema check reads the file first. A list written as one string, a date written in words or an e-mail address without its @ stops the run here, before anything is built.
  3. The Typst build. Typst reads a template and the data and lays out the PDF. The template decides the look, so a change of look never touches the facts, and a change of facts never touches the look.
  4. The checks of the PDF. The checks read the PDF the way software will: its title, its tags, its page count and the order of the text a parser gets.
  5. A step fails. Had any step failed, the run would stop there and keep nothing. The fix goes into resume.json or the template, and the next push runs every step again.
  6. The PDF is kept. Every step passed, so the run keeps the PDF: as the run's artifact to download, or published where readers of a portfolio can find it.

Other tools fit the same pipeline. RenderCV, an MIT-licensed Python tool for Python 3.12 or newer, turns a resume written in YAML into a PDF. An HTML page printed to PDF works too, and the LaTeX module of this track builds the same way with latexmk. Whatever makes the PDF, the extraction checks of the formatting lesson still apply to it.

Checkpoint

Check: One data file, a template and checks

  1. Question 1 of 2 Asha moves from Pune to Bengaluru. With her resume kept as code, what does she change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: One value in resume.json, and every copy built from it changes

    Resume as code splits the facts, the look and the checks into separate parts. A fact such as the city changes once, in resume.json, and the next build carries it into every copy.

  2. Question 2 of 2 A push turns Asha's list of infrastructure skills into one string, "Docker, Kubernetes, Terraform". Which step of the pipeline stops it, and what is kept?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The schema check stops it, before the build, and no PDF is kept

    Each step reads what the step before it made, and the first step that fails stops the run. A wrong type is the schema check's job, so the run ends there and keeps nothing.

The schema checks the shape of your data, not its truth

The schema of the @jsonresume/schema package, version 1.3.1, is written in JSON Schema draft-07. It gives each value a type: highlights is a list of strings, email must look like an e-mail address, a profile’s url must be a full address, and a date must be written YYYY, YYYY-MM or YYYY-MM-DD. The check below runs the package’s own validate() function on three copies of Asha’s data: her real file, one with five mistakes, and one with two mistakes that the schema has no rule for.

The check, and the two flawed copies it reads

resume/validate.mjs

// Checks each file named on the command line against the JSON Resume schema, with the validate() function that the
// @jsonresume/schema package exports (it uses the jsonschema library). Prints every problem with the path of the value
// that broke a rule, and exits with status 1 if any file fails, so a CI step stops there.
//   node validate.mjs resume.json [more.json …]
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);
const resumeSchema = require('@jsonresume/schema');
const { version } = require('@jsonresume/schema/package.json');

// The schema's date pattern is long; its errors name it by where the schema defines it.
const DATE_PATTERN = resumeSchema.schema.definitions.iso8601.pattern;

let failed = 0;
for (const file of process.argv.slice(2)) {
  const resume = JSON.parse(readFileSync(file, 'utf8'));
  resumeSchema.validate(resume, (errors, valid) => {
    if (valid) {
      console.log(`${file} is valid (JSON Resume ${version})`);
      return;
    }
    failed++;
    console.log(`${file}: ${errors.length} problem${errors.length === 1 ? '' : 's'}`);
    for (const e of errors) {
      const path = e.property.replace(/^instance\.?/, '') || '(the whole file)';
      const message = e.message.replace(JSON.stringify(DATE_PATTERN), '#/definitions/iso8601');
      console.log(`  ${path}: ${message}`);
    }
  });
}
process.exitCode = failed ? 1 : 0;

resume/broken.json

{
  "basics": {
    "name": "Asha Verma",
    "label": "Backend engineer",
    "email": "asha.verma(at)example.com",
    "summary": "Backend engineer with three years of Python and PostgreSQL services for a logistics platform, who cuts latency and pages with measured fixes.",
    "location": { "city": "Pune", "countryCode": "IN" },
    "profiles": [
      { "network": "GitHub", "username": "asha-verma-example", "url": "github.com/asha-verma-example" }
    ]
  },
  "work": [
    {
      "name": "Example Logistics",
      "location": "Pune",
      "position": "Software Engineer",
      "startDate": "July 2023",
      "highlights": [
        "Cut p95 API latency from 420 ms to 180 ms with PostgreSQL indexes and a Redis cache.",
        "Built REST APIs in Python for route lookups, serving 3,000 requests a minute at peak.",
        "Built a Kafka consumer for 2 million shipment events a day, with idempotent writes and Grafana alerts.",
        "Moved 14 services to Docker and Kubernetes with Terraform, so a deploy takes 9 minutes instead of 40.",
        "Resolved 23 production incidents as one of six on-call engineers, and wrote 11 runbooks.",
        "Built a nightly SQL job that reconciles 400,000 shipment rows against the billing database."
      ]
    },
    {
      "name": "Example Analytics",
      "location": "Bengaluru",
      "position": "Software Engineering Intern",
      "startDate": "2022-01",
      "endDate": "2022-06",
      "highlights": "Wrote Python data-quality checks that stopped 37 bad rows a week before they reached dashboards."
    }
  ],
  "projects": [
    {
      "name": "Bus pass renewal bot",
      "keywords": ["Python", "FastAPI", "PostgreSQL"],
      "url": "https://github.com/asha-verma-example/bus-pass-bot",
      "highlights": [
        "Renews a monthly bus pass in 3 messages instead of a 20-minute queue.",
        "Packaged with Docker, so it runs on one small server."
      ]
    }
  ],
  "skills": [
    { "name": "Backend", "keywords": ["Python", "Go", "PostgreSQL", "Redis", "Kafka"] },
    { "name": "Infrastructure", "keywords": "Docker, Kubernetes, Terraform, Grafana, Git" }
  ],
  "education": [
    {
      "institution": "Example Institute of Technology",
      "studyType": "B.Tech.",
      "area": "Computer Science",
      "endDate": "2022",
      "score": "CGPA 8.2/10"
    }
  ]
}

resume/typo.json

{
  "basics": {
    "name": "Asha Verma",
    "label": "Backend engineer",
    "email": "asha.verma@example.com",
    "summary": "Backend engineer with three years of Python and PostgreSQL services for a logistics platform, who cuts latency and pages with measured fixes.",
    "location": { "city": "Pune", "countryCode": "IN" },
    "profiles": [
      { "network": "GitHub", "username": "asha-verma-example", "url": "https://github.com/asha-verma-example" }
    ]
  },
  "work": [
    {
      "name": "Example Logistics",
      "location": "Pune",
      "position": "Software Engineer",
      "startDate": "2023-07",
      "highlights": [
        "Cut p95 API latency from 420 ms to 180 ms with PostgreSQL indexes and a Redis cache.",
        "Built REST APIs in Python for route lookups, serving 3,000 requests a minute at peak.",
        "Built a Kafka consumer for 2 million shipment events a day, with idempotent writes and Grafana alerts.",
        "Moved 14 services to Docker and Kubernetes with Terraform, so a deploy takes 9 minutes instead of 40.",
        "Resolved 23 production incidents as one of six on-call engineers, and wrote 11 runbooks.",
        "Built a nightly SQL job that reconciles 400,000 shipment rows against the billing database."
      ]
    },
    {
      "name": "Example Analytics",
      "location": "Bengaluru",
      "position": "Software Engineering Intern",
      "startDate": "2022-01",
      "endDate": "2022-16",
      "highlights": [
        "Wrote Python data-quality checks that stopped 37 bad rows a week before they reached dashboards.",
        "Built an Airflow pipeline that loads 12 CSV feeds into PostgreSQL every night.",
        "Rewrote a pandas report in SQL, cutting its run time from 40 minutes to 4."
      ]
    }
  ],
  "projects": [
    {
      "name": "Bus pass renewal bot",
      "keywords": ["Python", "FastAPI", "PostgreSQL"],
      "url": "https://github.com/asha-verma-example/bus-pass-bot",
      "hightlights": [
        "Renews a monthly bus pass in 3 messages instead of a 20-minute queue.",
        "Packaged with Docker, so it runs on one small server."
      ]
    }
  ],
  "skills": [
    { "name": "Backend", "keywords": ["Python", "Go", "PostgreSQL", "Redis", "Kafka"] },
    { "name": "Infrastructure", "keywords": ["Docker", "Kubernetes", "Terraform", "Grafana", "Git"] }
  ],
  "education": [
    {
      "institution": "Example Institute of Technology",
      "studyType": "B.Tech.",
      "area": "Computer Science",
      "endDate": "2022",
      "score": "CGPA 8.2/10"
    }
  ]
}
Three copies of Asha's data against the JSON Resume schema Python · validate_session.py
from session import files, node_modules, run

# Three copies of Asha's resume data against the JSON Resume schema 1.3.1: her real one, one with five mistakes, and
# one with two mistakes the schema has no rule for (a misspelt key and a month that does not exist). The project has
# @jsonresume/schema 1.3.1 installed, as `npm install` with its package.json gives it.
files("resume/resume.json", "resume/broken.json", "resume/typo.json", "resume/validate.mjs", "resume/package.json")
node_modules()
run("""
node validate.mjs resume.json
!node validate.mjs broken.json
node validate.mjs typo.json
grep -n -E 'hightlights|2022-16' typo.json
""")

Output

$ node validate.mjs resume.json
resume.json is valid (JSON Resume 1.3.1)
$ node validate.mjs broken.json
broken.json: 5 problems
  basics.email: does not conform to the "email" format
  basics.profiles[0].url: does not conform to the "uri" format
  work[0].startDate: does not match pattern #/definitions/iso8601
  work[1].highlights: is not of a type(s) array
  skills[1].keywords: is not of a type(s) array
[exit status 1]
$ node validate.mjs typo.json
typo.json is valid (JSON Resume 1.3.1)
$ grep -n -E 'hightlights|2022-16' typo.json
32:      "endDate": "2022-16",
45:      "hightlights": [

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 validate_session.py

resume.json passed, and broken.json failed five times, each with the path of the value that broke a rule: an e-mail address written with (at), a GitHub address without https://, a start date written in words, and two lists written as single strings. typo.json passed too, with two mistakes in it. The schema allows keys it does not define at every level, so hightlights is just an extra key, and the template, which reads highlights, finds no lines for that project. The date pattern allows month digits from 00 to 19, so 2022-16 passed as well.

Reading valid as correct

Wrong: “CI says resume.json is valid, so it is right.” Right: valid means every value the schema has a rule for has the right shape. A misspelt key, a sixteenth month and a line that claims too much are all valid.

MySmartCoPilot’s JSON Schema Validator reads the schema’s draft-07 and finds the same five problems in broken.json, each with its line and column:

JSON Schema Validator Paste your resume.json and the JSON Resume schema to see every problem with its line, in your browser.

Key idea

A schema check proves the shape of your data, not its truth: add checks of your own for what it lets through, such as keys it does not define and dates no calendar has.

Checkpoint

Check: What the schema checks, and what it lets through

  1. Question 1 of 2 typo.json passed the schema check. Which statement about it is true?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It can still lose a project's lines, because the schema accepts keys it does not define

    The JSON Resume schema allows keys it does not define at every level, so a misspelt key passes. The file is valid and still wrong, which is why the lab adds a check for unknown keys.

  2. Question 2 of 2 Which address for Asha's GitHub profile passes the schema's uri format?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: https://github.com/asha-verma-example

    The schema gives a profile's url the format uri, which needs a scheme such as https. broken.json wrote the address without it, and the check reported basics.profiles[0].url.

A tagged, titled PDF/UA-1 resume from the data, measured Pro: opens with a Pro, Premium or Ultimate pass

Typst turns the data into the PDF, and its Part one of PDF/UA, the PDF standard for universal access: tagged content, a document title, the text language and a structure that screen readers and other assistive software can follow. mode refuses a file that assistive software would struggle with. This part builds Asha’s PDF, reproduces the refusals of two first drafts, and reads the result back the way parsers do.

In this part:

  • A template that reads the data and adds no facts
  • Two first drafts that PDF/UA-1 refuses, and the fix for each
  • What two extractors read, and a valid file that breaks the build

4 runnable examples (JavaScript, Python, TYP) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A CI workflow that stops a broken resume, run on four pushes Pro: opens with a Pro, Premium or Ultimate pass

A workflow runs the same three steps on every push and keeps a PDF only when all of them pass. This part runs the job on four pushes, each with one mistake a step exists to catch, then audits the workflow file itself.

In this part:

  • The workflow: three checked steps, pinned tools and a read-only token
  • Four pushes, and the step that stops each broken one
  • A weak workflow that actionlint passes and zizmor flags ten times

4 runnable examples (Python, YAML, check_pdf.py, resume.yml) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Variants from one source, and a phone number that never goes public Pro: opens with a Pro, Premium or Ultimate pass

A variant chooses true lines from the same file, and the phone number lives in a file the repository never holds. This part builds two variants, breaks one on purpose, and catches a leak before anything is published.

In this part:

  • Variants name their lines, so a rewording fails loudly
  • The phone number stays in a file the repository never holds
  • What the workflow may build, and where each file may go

4 runnable examples (Python, make_variants.py, variants.json) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: catch what the schema lets through

The lab has two steps, the two checks the schema leaves to you. Both read Asha’s data as Python dictionaries, the way json.load returns them:

Code lab · 2 steps

Catch what the schema lets through

Write the two checks that run before the build: keys the schema does not define, and dates no calendar has.

  1. Find the keys the schema does not define To do
  2. Find the dates no calendar has To do

Exercise · Easy · Python

Find the keys the schema does not define

The JSON Resume schema allows keys it does not define, so a misspelt hightlights passes validation and the template shows no bullets for that entry. Write unknown_keys(resume) in unknown_keys.py: it returns the path of every key that the schema does not define, in the order the file holds them.

  • KNOWN in the starter lists the keys the schema defines: KNOWN[""] for the top level, KNOWN["basics"], KNOWN["basics.location"] and KNOWN["basics.profiles"] for the parts of basics, and KNOWN["work"], KNOWN["projects"] and the other sections for each item of that section's list.
  • A path joins keys with dots and gives a list item's position in brackets: eduction, basics.emial, basics.profiles[0].userName, projects[0].hightlights.
  • Look one level into each item of a section's list and into basics, basics.location and each profile. Values inside a known key, such as the strings of highlights, are not keys.
  • A section you do not know is reported once, by its own name, and nothing inside it is read.
  • "In the order the file holds them" holds at every level: a bad key of basics.location comes before a bad key of basics that the file writes after location.

The sample tests use parts of Asha's typo.json from this lesson.

Starter code · unknown_keys.py

# The keys that the JSON Resume schema 1.3.1 defines, by where they sit (its schema.json).
KNOWN = {
    "": ["$schema", "basics", "work", "volunteer", "education", "awards", "certificates", "publications", "skills",
         "languages", "interests", "references", "projects", "meta"],
    "basics": ["name", "label", "image", "email", "phone", "url", "summary", "location", "profiles"],
    "basics.location": ["address", "postalCode", "city", "countryCode", "region"],
    "basics.profiles": ["network", "username", "url"],
    "work": ["name", "location", "description", "position", "url", "startDate", "endDate", "summary", "highlights"],
    "volunteer": ["organization", "position", "url", "startDate", "endDate", "summary", "highlights"],
    "education": ["institution", "url", "area", "studyType", "startDate", "endDate", "score", "courses"],
    "awards": ["title", "date", "awarder", "summary"],
    "certificates": ["name", "date", "url", "issuer"],
    "publications": ["name", "publisher", "releaseDate", "url", "summary"],
    "skills": ["name", "level", "keywords"],
    "languages": ["language", "fluency"],
    "interests": ["name", "keywords"],
    "references": ["name", "reference"],
    "projects": ["name", "description", "highlights", "keywords", "startDate", "endDate", "url", "roles", "entity",
                 "type"],
}


def unknown_keys(resume):
    """The path of every key of `resume` that the schema does not define, in the file's order."""
    # Replace this comment and the line below with the rules of the prompt.
    return []
The sample tests · test_unknown_keys.py
from unknown_keys import unknown_keys

BOT = {"name": "Bus pass renewal bot", "keywords": ["Python"], "highlights": ["Renews a pass in 3 messages."]}


def test_known_keys_only():
    """returns nothing for a resume whose keys the schema all defines"""
    resume = {"basics": {"name": "Asha Verma", "location": {"city": "Pune"}}, "projects": [BOT]}
    assert unknown_keys(resume) == []


def test_misspelt_project_key():
    """finds hightlights in a project, with the project's position"""
    typo = {"name": "Bus pass renewal bot", "keywords": ["Python"], "hightlights": ["Renews a pass in 3 messages."]}
    assert unknown_keys({"projects": [BOT, typo]}) == ["projects[1].hightlights"]


def test_top_level_and_basics():
    """reports an unknown section by its own name, and a misspelt key of basics"""
    resume = {"basics": {"name": "Asha Verma", "emial": "asha.verma@example.com"}, "eduction": [{"institution": "X"}]}
    assert unknown_keys(resume) == ["basics.emial", "eduction"]


def test_location_and_profiles():
    """looks into basics.location and into every profile"""
    resume = {"basics": {"location": {"city": "Pune", "pincode": "411001"},
                         "profiles": [{"network": "GitHub", "userName": "asha-verma-example"}]}}
    assert unknown_keys(resume) == ["basics.location.pincode", "basics.profiles[0].userName"]


def test_file_order():
    """keeps the order of the file, entry by entry"""
    resume = {"work": [{"name": "Example Logistics", "start": "2023-07", "highlights": []},
                       {"name": "Example Analytics", "end": "2022-06"}],
              "skills": [{"name": "Backend", "keyword": ["Go"]}]}
    assert unknown_keys(resume) == ["work[0].start", "work[1].end", "skills[0].keyword"]


def test_order_inside_basics():
    """reports the keys inside basics in the order the file writes them"""
    resume = {"basics": {"location": {"city": "Pune", "pin": "411001"}, "emial": "asha.verma@example.com"}}
    assert unknown_keys(resume) == ["basics.location.pin", "basics.emial"]
A hint

Walk the file in its own order. For each top-level key, report it when KNOWN[""] does not hold it. Inside basics, look at each key in turn, and go into location or profiles when you reach them. For a known section, check the keys of each item: [f"{section}[{i}].{key}" for key in item if key not in KNOWN[section]].

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Exercise · Easy · Python

Find the dates no calendar has

The schema's date pattern lets 2022-16 through, because its month digits may run from 00 to 19 and its day digits from 00 to 39. Write bad_dates(resume) in dates.py: it returns (path, value) for every date of resume that no calendar has, in the order the file holds them.

  • A date is the value of startDate, endDate, date or releaseDate in an item of a section's list, such as work[1].endDate or awards[0].date. The path is the section, the item's position in brackets, a dot and the key.
  • A date is written YYYY, YYYY-MM or YYYY-MM-DD. A value of any other shape, such as 2023-7, is bad too.
  • A real date has a month from 01 to 12 and a day that its month has. February's last day depends on the year; calendar.monthrange(year, month) knows it.

The sample tests start from Asha's typo.json of this lesson.

Starter code · dates.py

import calendar
import re

DATE_KEYS = ("startDate", "endDate", "date", "releaseDate")


def bad_dates(resume):
    """(path, value) for every date of `resume` that no calendar has, in the file's order."""
    # Replace this comment and the line below with the rules of the prompt.
    return []
The sample tests · test_dates.py
from dates import bad_dates

LOGISTICS = {"name": "Example Logistics", "startDate": "2023-07"}


def test_month_sixteen():
    """finds the internship's end date of typo.json, 2022-16"""
    work = [LOGISTICS, {"name": "Example Analytics", "startDate": "2022-01", "endDate": "2022-16"}]
    assert bad_dates({"work": work}) == [("work[1].endDate", "2022-16")]


def test_real_dates_pass():
    """accepts a year alone, a month, and the leap day of a leap year"""
    resume = {"work": [LOGISTICS], "education": [{"endDate": "2022"}], "awards": [{"date": "2024-02-29"}]}
    assert bad_dates(resume) == []


def test_days_that_do_not_exist():
    """refuses a leap day outside a leap year, day 31 of a 30-day month, and month 00"""
    resume = {"awards": [{"date": "2023-02-29"}, {"date": "2021-04-31"}], "certificates": [{"date": "2022-00"}]}
    assert bad_dates(resume) == [("awards[0].date", "2023-02-29"), ("awards[1].date", "2021-04-31"),
                                 ("certificates[0].date", "2022-00")]


def test_bad_shape():
    """refuses a value of another shape, such as a one-digit month"""
    assert bad_dates({"work": [{"name": "Example Logistics", "startDate": "2023-7"}]}) == [("work[0].startDate", "2023-7")]


def test_every_date_key_in_file_order():
    """reads startDate, endDate, date and releaseDate in the order the file holds them"""
    resume = {"publications": [{"name": "Kafka notes", "releaseDate": "2024-13"}],
              "projects": [{"name": "Bot", "endDate": "2023-02-30", "startDate": "2023-19"}]}
    assert bad_dates(resume) == [("publications[0].releaseDate", "2024-13"), ("projects[0].endDate", "2023-02-30"),
                                 ("projects[0].startDate", "2023-19")]
A hint

Match the whole value with re.fullmatch(r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?", value): no match means a bad shape, and the groups give the year, the month and the day (None where the value stops). calendar.monthrange(year, month)[1] is the number of days in that month.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then answer questions on the pipeline and the schema. Each round starts with the idea you have found hardest:

Practice that adapts to you

Practise this lesson

7 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 Asha wants her resume in two columns. Which part of the pipeline changes?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: The template only

    Facts live in the data and the look in the template. Changing one never touches the other, which is what lets a change be made once.

  2. Question 2 of 7 Why does the schema check run before the Typst build rather than after it?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: So that data with a wrong shape stops the run before anything is built, with the path of the value at fault

    The pipeline checks the data first, so a mistake in it is reported as a mistake in the data, by its path, and nothing is built from it.

  3. Question 3 of 7 A friend keeps one PDF and edits it in a PDF editor before each application. Which problem of hers does resume as code remove, and which does it not?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: It removes copies that drift apart, but not a false or weak line, which the data would carry into every copy

    Resume as code makes every copy agree with one source, and the schema makes that source well formed. Neither makes a line true, which is why the review and tailoring rules of this track still apply.

  4. Question 4 of 7 What does the JSON Resume schema 1.3.1 do with a key it does not define, such as hightlights?

    Choose one answer.

    Show the answer: Question 4 of 7

    Answer: It accepts it, because it allows keys it does not define at every level

    The schema allows extra keys everywhere, so a misspelt key is valid. Only a check of your own, such as the lab's, lists the keys the schema does not define.

  5. Question 5 of 7 Which of these dates passes the JSON Resume 1.3.1 schema?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: 2022-16

    The pattern checks the shape of a date, not whether a calendar has it. That is why the lab's second step checks every date against a calendar.

  6. Question 6 of 7 The check reports work[0].startDate: does not match pattern #/definitions/iso8601. Which value fixes it?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: 2023-07

    The schema's iso8601 definition allows YYYY, YYYY-MM and YYYY-MM-DD. A start in the seventh month of the year is written 2023-07.

  7. Question 7 of 7 Which check, added to the pipeline, would have stopped typo.json before the build?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: A check that lists every key the schema does not define, run after the schema check

    What the schema lets through needs a check of its own. The lab's first step lists unknown keys and its second finds dates no calendar has.

Interview questions

Warm-up (fresher to mid-level): What does “resume as code” mean, and why would you do it?

It means keeping a resume’s facts in one data file, such as a JSON Resume file in a repository, and building every copy from it with a template and a script. A date changes once and every copy changes with it, the history shows what changed, and checks run on every push, so a broken file never becomes a PDF. It also makes a tailored variant a choice of true lines from the same file. The cost is the setup, so it pays off for someone who updates and tailors often.

Three questions of this module’s bank go further:

  1. Fresher to mid

    Your CI says resume.json is valid against the JSON Resume schema, but the PDF it builds shows no lines under one of your projects. What happened, and what do you add so it cannot happen again?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    You want your resume repository to be public, so reviewers can see the pipeline, but employers need your phone number on the PDF they receive. How do you set it up?

    Model answer, Pro. First step and follow-ups

  3. Fresher to senior

    A recruiter who uses a screen reader tells you your resume PDF has no headings to jump between. How do you build PDFs that work for her and for a parser, and prove it on every change?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Into which three parts does resume as code split a resume?

    Data that says what is true (names, dates, lines of work), a template that sets how it looks, and checks that every copy must pass. A date changed once in the data changes in every copy built from it.

  2. What is JSON Resume?

    An open format that keeps a resume in one JSON file with sections such as basics, work, education, skills and projects, described by a JSON Schema the project publishes under the MIT licence.

  3. What do Typst and continuous integration each do in a resume pipeline?

    Typst's compiler reads a template and the JSON data and writes a PDF, tagged by default. CI runs the steps on every push and stops at the first that fails, so a broken resume never becomes a PDF.

  4. Which shapes does the JSON Resume schema check?

    That highlights is a list of strings, an email looks like an e-mail address, a profile url is a full address, and each date is written as a year, a year and month, or a full year-month-day.

  5. Does a resume.json that passes the schema check hold correct data?

    Not necessarily. Valid only means that every value the schema has a rule for has the right shape; a misspelt key, an impossible month and an inflated claim all pass.

  6. Which two mistakes does the JSON Resume schema let through?

    Keys it does not define, at any level, so a misspelt “hightlights” is just an extra key the template never reads, and month digits up to 19, so a sixteenth month passes. Add checks of your own for both.

  7. Which other tools fit the same resume pipeline?

    RenderCV, an MIT-licensed Python tool that turns a resume written in YAML into a PDF, an HTML page printed to PDF, or LaTeX built with latexmk. Whatever makes the PDF, check its extracted text.

Key takeaways

  • Resume as code keeps the facts in one data file and builds every copy from it, so a change is made once.
  • JSON Resume gives the data its sections and types, and a schema check stops a wrong shape before anything is built.
  • A valid file can still be wrong: the schema accepts keys it does not define and a sixteenth month, so check for both.
  • Typst writes a tagged PDF from the data, and CI runs every step on each push and stops at the first that fails.

Terms in this lesson

JSON Resume
An open format for a resume as one JSON file, with sections such as basics, work, education, skills and projects, defined by a JSON Schema that the project publishes under the MIT licence.
JSON Schema
A JSON document that describes what another JSON document may hold: the type of each value, the keys it may have and the patterns its strings must match, so that software can check a document against it.
Typst
A typesetting system with its own markup and scripting language. Its compiler turns a .typ file, and any data that file reads, into a PDF, and it writes Tagged PDF by default.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.