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.
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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.
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"
}
]
} 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:
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
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
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.
KNOWNin the starter lists the keys the schema defines:KNOWN[""]for the top level,KNOWN["basics"],KNOWN["basics.location"]andKNOWN["basics.profiles"]for the parts ofbasics, andKNOWN["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.locationand each profile. Values inside a known key, such as the strings ofhighlights, 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.locationcomes before a bad key ofbasicsthat the file writes afterlocation.
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]].
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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,dateorreleaseDatein an item of a section's list, such aswork[1].endDateorawards[0].date. The path is the section, the item's position in brackets, a dot and the key. - A date is written
YYYY,YYYY-MMorYYYY-MM-DD. A value of any other shape, such as2023-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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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:
-
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?
-
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?
-
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?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
- JSON Resume schema (JSON Resume)
- @jsonresume/schema (npm)
- jsonresume.org (the JSON Resume monorepo) (JSON Resume)
- JSON Schema Draft-07 (JSON Schema)
- PDF (Typst reference) (Typst)
- Document (Typst reference) (Typst)
- JSON (Typst reference) (Typst)
- Typst 0.15.1 (Typst)
- rendercv (Python Package Index)
- Secure use reference (GitHub Docs)
- Using secrets in GitHub Actions (GitHub Docs)
- Downloading workflow artifacts (GitHub Docs)
- Managing GitHub Actions settings for a repository (GitHub Docs)
- Removing sensitive data from a repository (GitHub Docs)
- npm ci (npm Docs)
- actionlint (actionlint)
- Audit Rules (zizmor) (zizmor)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress