ajustacv-resume-schema
v0.2.0
Published
Canonical ResumeDocument schema, validators and fixtures shared by ajustacv.com and api.ajustacv.com
Maintainers
Readme
ajustacv-resume-schema
The canonical ResumeDocument model (issue #69), shared by the AjustaCV
frontend and API so neither repo keeps its own copy.
Zod schemas are the source of truth. Every exported TypeScript type is inferred
from them with z.infer, so the runtime validator and the compile-time types
cannot drift apart.
Install
ajustacv.com consumes it through the Bun workspace and always builds from
source in this directory, so schema edits need no release:
{ "dependencies": { "ajustacv-resume-schema": "workspace:*" } }api.ajustacv.com is a separate repo whose Dockerfile copies only its own
files, so it cannot reach this directory at build time and depends on the
published version instead:
{ "dependencies": { "ajustacv-resume-schema": "^0.1.0" } }The published tarball is source-only and carries no rights (UNLICENSED) —
publishing keeps the API's Docker build credential-free, it does not make the
repo open source.
Usage
import { validateResumeDocument, type ResumeDocument } from "ajustacv-resume-schema";
const result = validateResumeDocument(await request.json());
if (!result.ok) return { status: 400, issues: result.issues };
const doc: ResumeDocument = result.document;Sample documents live behind a separate entry point so they never reach the frontend bundle:
import { FULL_RESUME, RESUME_FIXTURES } from "ajustacv-resume-schema/fixtures";The model
ResumeDocument
├── header ResumeHeader name, headline, contacts, links, optional photo
├── sections ResumeSection[] ordered; discriminated union over 10 types
├── design DesignSettings templateId, free-hex accent, curated fontId, A4
└── meta DocumentMeta schemaVersion, targetRole, nullable ownerIdThe ten section types are summary, experience, education, skills,
languages, certifications, projects, achievements, strengths and
custom. achievements and strengths share one entry shape — a title and a
one-line description — and exist as their own types so a template can set them
apart and an ATS can read them as what they are. custom is the
catch-all that keeps parsing lossless — content an extractor cannot classify
lands there rather than being dropped. Duplicates are allowed: a resume may
carry two custom sections, or two experience ones.
summary is the single-entry section (it has entry, not entries), because
a resume has one summary paragraph and a list would allow states no UI can
produce. Every other section holds an array.
Conventions worth knowing
RichLineis one line of an inline markdown subset —**bold**,*italic*,[label](url). Block structure is modelled in the schema itself (bullets are arrays, not-prefixes), so a line break inside aRichLinemeans the producer flattened structure away and is rejected.- Dates are structured
YearMonth(monthoptional) inside aDateRangewith acurrentflag. An ongoing range must have a nullend, andendmay not precedestart. UsecompareYearMonthto order them; a missing month sorts as January. - Hiding is non-destructive. A section carries
visible, and every entry carrieshiddenFields. The values stay in the document and templates skip rendering them, so nothing is lost by tucking a detail away. - Page breaks are
pageBreakBeforeflags on sections and on entries. - URLs are restricted to http/https, and markdown links inside a
RichLineto http/https/mailto. Resume content comes from parsed uploads and AI output and is rendered ashrefs in the editor and the exported PDF, and a barez.url()acceptsjavascript:targets. - Unknown keys are stripped, not rejected, so a newer producer degrades to
a valid document. Real incompatibility is caught by
meta.schemaVersion. - Ids must be unique among sections, and among entries within a section. They anchor editor selection, undo/redo and version diffs, so a duplicate would make those features address the wrong node.
Validation
validateResumeDocument(input) returns a discriminated result rather than
throwing:
{ ok: true, document: ResumeDocument } | { ok: false, issues: SchemaIssue[] }Each issue carries a dotted path — sections.1.entries.0.dateRange.end — so
the editor can anchor the error to the field that caused it. All problems are
reported at once, not just the first.
Also exported: parseResumeDocumentJson for text arriving from the Draft Slot
or a request body, isResumeDocument as a type guard, assertResumeDocument
for call sites where invalid input is a bug rather than user input, and
formatIssues for logging.
Fixtures
| Fixture | Covers |
| --- | --- |
| FULL_RESUME | realistic pt-BR resume, all 8 section types, a hidden field, a page break |
| MINIMAL_RESUME | the smallest valid document — an untouched anonymous draft |
| ALL_SECTION_TYPES_RESUME | one entry per section type, plus a hidden section |
| HIDDEN_FIELDS_RESUME | every hideable field, hidden, values retained |
| PAGE_BREAKS_RESUME | manual breaks at both section and entry level |
Each is authored as schema input and parsed at module load, so a fixture that falls behind a schema change fails on import instead of quietly teaching tests the wrong shape.
Releasing
The frontend builds from source, so a release is only needed when the API needs a schema change:
cd packages/resume-schema
bun test && bunx tsc --noEmit
npm version patch # minor instead for a breaking change while on 0.x
npm publishThen bump the range in api.ajustacv.com/package.json and run bun install
there. While the version is 0.x, a caret range allows patches but not minors,
so a breaking change cannot reach the API silently.
Only src ships, minus the tests — bun test and bunx tsc --noEmit must both
pass before publishing, since consumers compile this source directly.
