JSON Resume Support Plan¶
Goal¶
Plan support for JSON Resume, the open-source JSON-based resume standard.
This page records design decisions and remaining implementation research for JSON Resume import and export.
Phase Scope¶
The initial implementation should cover unsigned JSON Resume import/export in
both portable and django-resume round-trip shapes, portable projection
validation, django-resume-specific extension validation, plugin-owned adapters,
conservative update defaults, reports, and storage of integration state. After
schema pinning, the initial implementation should also validate complete
documents against the pinned schema when that schema permits the extension
points in use; otherwise only the portable projection receives upstream-schema
validation. Conditional on the pinned schema allowing them, the initial phase may
also export, validate, and preserve item-level x-django-resume fields, but
not consume them back into plugin storage. The deferred trusted-round-trip phase
should add provenance signing, canonicalization, freshness and replay protection,
trusted deletion round-trips, automatic fallback restoration, and consumption of
item-level x-django-resume fields back into plugin storage during
whole-array or whole-section replacement by the owning adapter.
This initial-phase scope is provisional until schema pinning determines which
extension points can be represented in a complete JSON Resume document.
Current Design Gates¶
Which exact upstream JSON Resume schema revision should be pinned for validation, and which extension points does that revision allow?
Which canonicalization implementation and freshness policy should gate the deferred trusted-round-trip phase?
What stable item identity model should support future merge-by-item updates and restoration of unknown item-level extensions?
What stronger child-process isolation and sandboxing limits should be required before adding PDF rendering, public previews, or support for unreviewed community themes in production deployments?
The Remaining Work section is the authoritative task breakdown. This section
only highlights design gates that affect the decisions below.
Directional Decisions¶
Decisions that depend on item-level extensions, complete-document upstream validation, or item-level restoration are conditional until the corresponding schema or stable-identity work is resolved. Stable item identity is future work and does not block the initial import/export design. The provenance signature model is conditional until the canonicalization implementation and trusted-import freshness policy are selected and tested. Until then, exports should be unsigned by default, imports should skip provenance verification, and imports should use untrusted-document defaults. Signing and verification should land together. The detailed sections below provide the normative detail for these summary decisions and should not contradict them.
django-resume should support both JSON Resume import and export. Treat JSON Resume as an interchange format: export should let users take django-resume data to other tools, and import should let users bootstrap or update a django-resume resume from existing JSON Resume data.
JSON Resume should map through plugin-owned adapters discovered through the existing plugin registry. A central coordinator should assemble, validate, import, and export the whole JSON Resume document, while each participating plugin owns the mapping for its own data. The trust terms used below are defined in Custom Data; until then, read them as referring to signed same-installation exports that satisfy the matching and freshness checks described there.
Unsupported custom plugin data should use a namespaced django-resume extension envelope. Use
meta.django_resumefor document-level metadata,meta.django_resume.pluginsfor fallback plugin payloads, andmeta.django_resume.preserved_extensionsfor preserved unknown extensions. Conditional on the pinned schema allowing the item type, use item-levelx-django-resumeextension fields only when the data belongs to a specific exported item and the owning adapter controls that item. Plugin adapters should still map data into standard JSON Resume fields whenever possible.Every registered content plugin should be classified for JSON Resume participation: mapped through a plugin-owned adapter, preserved through an export-safe fallback payload, or omitted with an import/export report entry. Plugins that declare nothing are omitted by default and reported. django-resume should not automatically dump raw adapterless plugin payloads into exported JSON Resume files.
Document-level JSON Resume integration state should not be stored in
Resume.plugin_databecause that field is plugin-owned. Add a dedicated core-owned JSON field such asResume.integration_dataand store JSON Resume envelope state underintegration_data["json_resume"].Validation should be layered. django-resume should validate a portable JSON Resume projection of exported and incoming documents against a pinned copy of the upstream schema before export or import. When the pinned schema permits django-resume’s chosen extension points, the complete extended document should also be validated against it. JSON Resume does not provide a reliable schema-version selector, so
meta.versionmust not be treated as the schema version. Imports should validate against django-resume’s pinned supported schema and report any explicit schema URI or canonical pointer that differs from the pinned schema. The upstream schema should not be fetched live at runtime. django-resume should also validatemeta.django_resumeand plugin import/export constraints with django-resume-specific validation.The import/export framework should live in core, and JSON Resume should ship as the first built-in integration. Core should own the adapter protocols, coordinator, validation flow, and built-in JSON Resume format while leaving room for future import/export formats.
Round-trip fidelity should be semantic, not byte-for-byte. django-resume should preserve supported standard JSON Resume fields and its own
meta.django_resumeextension data according to the phase-aware trust and opt-in rules below. Unknown third-party extension fields should be preserved on a best-effort basis when they can be stored without conflicting with mapped plugin data. Imports should report unsupported or dropped fields explicitly.Import update behavior should be explicit and conservative. The initial design should support
createandupdatemodes.createcreates a new resume from JSON Resume data for a caller-supplied owner and caller-suppliedResume.slug. The slug is django-resume metadata, not a standard JSON Resume field; views can collect it from the request, and management commands can require it as an argument.createshould fail if that globally unique slug already exists. Overwrite-on-create is out of scope for the initial design.updateupdates an existing resume selected by the caller, such as by primary key in a view or command argument in a CLI, and does so transactionally by replacing only plugin data owned by participating adapters, restorable fallback payloads from trusted target-matching exports, or explicitly confirmed untrusted fallback payloads whose declared source paths are present in the imported document. Present-empty untrusted fallback payloads require bothrestore_untrusted_fallbacksandapply_empty_fallbacks. Replacement is per declared source path. Present-empty replacement becomes a trusted destructive default only after the provenance signature and freshness model is available. Until then, and for untrusted or target-mismatched documents, present empty standard sections are no-op updates unless the caller explicitly opts into applying them. Absent adapter-owned standard source paths remain unchanged unlessclear_missingis enabled. Participating plugins with absent source paths should be left unchanged by default. A separateclear_missingoption may clear participating plugin data whose adapter-owned standard source paths are absent from the imported document, but it should default toFalseand should always require a preview plus per-section or per-plugin confirmation because missing paths are not trusted destructive defaults.clear_missingdoes not apply tometa.django_resume.preserved_extensions; preserved-extension replacement is controlled byreplace_preserved_extensions.clear_missingapplies to fallback-backed plugin data only whenclear_missing_fallbacksis also enabled. Merge-by-item identity and merge conflict handling should stay out of scope until adapters define stable identity and conflict rules. Imports should report unsupported, ambiguous, or ignored data instead of silently dropping it. Updating a resume is caller-directed and may overwrite the selected target’s content; view and CLI flows should require explicit confirmation naming the target resume. Trusted destructive defaults should apply only for trusted target-matching exports; mismatches should use untrusted-document defaults and produce a warning.Existing JSON Resume themes should be reused by rendering a validated JSON Resume document with the Node-based JSON Resume theme ecosystem, not by porting those templates into Django templates. Theme rendering is a presentation step layered on top of export. It should consume a portable JSON Resume projection by default, with django-resume’s private extension envelope stripped unless the caller explicitly requests a private same-application artifact.
Relationship To Other Planning Work¶
JSON Resume support should be designed alongside the structured-data API needed by application workflows. It should also stay separate from page layout and routing concerns: JSON Resume can describe common resume facts, while page plugins and application workflows may need django-resume-specific metadata. The extension envelope should not be used to preserve layout, routing, permission, token, workflow, or delivery state unless a future django-resume import/export format explicitly opts into that application-private data.
JSON Resume’s own URL-based import/export endpoints live under the resume slug,
so they are ordinary non-page routes that must be ordered ahead of the page
registry’s catch-all <slug:slug>/ route; the page registry owns that
ordering.
See Application Workflows And Agentic Outreach and Pluggable Resume Pages.
First Export Slice Design Notes¶
The first implemented slice was deliberately consumer-driven: export one resume to a JSON Resume v1.0.0 document, and use that concrete consumer to shape the reusable structured-facts API. This avoided designing a broad structured-data abstraction in isolation while still creating the primitive needed by later import/export, workflow, and agent-assisted features.
The slice introduced get_structured_data(resume) -> dict on content
plugins. The method returns normalized, JSON-serializable, format-neutral facts
with stable field names and machine-readable values. Markdown stays as source
text, media references are resolved by the owning plugin, and dates are returned
as stored so format adapters can decide whether to emit, normalize, or report
them. Plugins opt out by returning an empty dictionary.
Format-specific mapping stays plugin-owned through export adapters exposed by
get_export_adapters(). An adapter declares the JSON Pointer paths it owns,
exports contributions for those paths, and returns notes for dropped fields or
lossy mappings. The coordinator discovers adapters through the plugin registry,
rejects overlapping scalar/object ownership, concatenates array contributions
only when every contributor declares the path multivalued, assembles the
document, validates it, and returns a report. Raw adapterless plugin payloads
are never dumped into the portable JSON Resume document.
Schema pinning was resolved against JSON Resume v1.0.0 and the schema is
vendored into the package. Validation is offline: django-resume must not fetch a
schema at runtime, and meta.version must not be treated as a schema version
selector. The pinned schema rejects new top-level keys, but the defined
meta object and section array items allow additional properties. That means
django-resume extension data belongs under meta.django_resume or item-level
namespaced fields when those phases need it, never at a new top-level key.
External JSON Resume Render Themes¶
Theme Vocabulary¶
The product should avoid using the unqualified word “theme” when both theme systems are visible. The preferred terms are:
page theme for django-resume’s own Django template set. A page theme controls built-in pages, page plugins, editing views, cover-letter content, and section plugin templates. The current stored value still lives in the existing
themeplugin data for compatibility.JSON Resume render theme for an npm package that renders a JSON Resume document, such as
jsonresume-theme-even. It is selected underintegration_data["json_resume"]["theme"]and is used only by JSON Resume render/export routes.theme catalog entry for a deployment-owned allowlist item that maps a stable local key to one pinned JSON Resume render theme package version plus display metadata.
The normal /<slug>/cv/ route should continue to render the django-resume CV
page with the selected page theme. The JSON Resume rendered view should remain
a visibly separate owner-only artifact under /<slug>/json-resume/rendered/.
If a future product flow offers a public JSON Resume-rendered CV, it should use
an explicit route, label, and permission model rather than silently changing the
meaning of the existing CV page.
Naming options considered:
Keep both concepts named “theme” and rely on route context. This is lowest churn but confusing in the editor and overview because one selector changes django-resume pages while the other runs an exported document through npm.
Rename django-resume themes to page themes and JSON Resume themes to render themes. This preserves current storage and code concepts while making the UI and docs clear. This is the preferred near-term path.
Rename JSON Resume themes to renderer presets or export styles. This is the least ambiguous product language, but it hides JSON Resume’s own ecosystem term and would create more translation between docs, package names, and UI.
Let a selected JSON Resume render theme replace the normal CV route. This makes theme selection feel unified, but it mixes two rendering pipelines, complicates permissions and token behavior, and makes cover/editing pages unaffected by the same setting. Keep it out of the default flow.
JSON Resume’s theme ecosystem is npm-based. The current JSON Resume
documentation recommends resumed for rendering resume.json with a theme;
themes are npm packages or local directories and conventionally expose a
render(resume) function that returns HTML. The JSON Resume themes page
lists hundreds of community packages. JSON Resume tooling can also produce PDFs
when the selected renderer and host environment supply the required browser or
PDF tooling.
django-resume should support that ecosystem by producing a clean JSON Resume document and, optionally, invoking a configured renderer. It should not try to translate npm themes into Django templates. Theme packages execute JavaScript during rendering and their returned HTML can contain arbitrary markup, styles, and scripts, so they must be treated as trusted server-side code installed by the site operator, not as user-submitted templates.
The viable implementation options are:
Documentation-only handoff. Document a workflow such as exporting a portable JSON Resume document and running
resumed render resume.json --theme jsonresume-theme-even --output resume.htmlin a local npm project. This has no runtime security or dependency risk and is enough for developers who are comfortable with the JSON Resume toolchain, but it does not provide an integrated django-resume command, preview, or PDF download.Managed renderer command. Add an optional
render_json_resumemanagement command that exports a resume, derives a portable projection, writes it to a temporary file, and invokes a configured local renderer such asnode_modules/.bin/resumed. The command would accept an allowlisted theme key and an output format such ashtmlorpdf. This remains a useful future operator and CI surface, even though owner-only HTML rendering is now available through web views.Authenticated in-app rendering. Add owner-only routes for theme search, installation, preview, or download. The current implementation uses this route for private HTML rendering with npm package-name validation, local cache installation, portable-projection rendering, private caching, and restrictive response headers. It should not expose arbitrary npm package installation to anonymous users. Public CV views and token-protected resume pages should not imply permission to render or download themed JSON Resume artifacts.
Curated in-app theme catalog. Ship or configure a deployment-owned catalog of approved JSON Resume themes with pinned npm package versions, display names, preview image metadata, and stable catalog keys. The owner UI should browse and preview this curated set rather than searching and installing arbitrary npm packages by default. This gives users a gallery-like theme chooser while keeping executable JavaScript dependencies under operator control.
Native Django rendering of JSON Resume themes. Implement a Django template pack that reads the JSON Resume projection. This may be useful as a built-in fallback, but it does not satisfy the goal of reusing the existing JSON Resume theme ecosystem and should not be the primary answer to external theme compatibility.
The implemented path is staged:
Document the external
resumedworkflow for users who only need interoperable themed HTML or PDF outside django-resume.Provide owner-only web routes that can render private HTML from a portable JSON Resume projection.
Treat the current owner-triggered npm search/install flow as a development and discovery aid, not the desired production default. It validates package names and keeps rendering owner-only, but installed themes still execute third-party JavaScript on the server.
Use a curated gallery of preconfigured, pinned themes as the default product flow. Operators decide which packages are available, at which versions, and when the catalog is refreshed. The application browses, previews, and applies only catalog themes unless dynamic npm installation is explicitly enabled.
Defer PDF support and deeper untrusted-theme sandboxing until the deployment contract for browser tooling, containers, process isolation, and host-level resource limits is explicit.
The renderer configuration should be explicit and deployment-owned. The current
implementation installs resumed and selected npm packages into
DJANGO_RESUME_JSON_RESUME_THEME_DIR and enforces a maximum HTML output size
with DJANGO_RESUME_JSON_RESUME_RENDER_MAX_BYTES. Theme catalog entries map
stable local keys to package names, exact versions, display metadata, preview
images, registry preview URLs, and enabled/disabled state. A checked-in default
catalog provides an initial curated list, and deployments can replace it with
DJANGO_RESUME_JSON_RESUME_THEME_CATALOG. Catalog preview and selection may
install the pinned package lazily into the cache; dynamic owner-triggered npm
search/install is hidden and rejected unless
DJANGO_RESUME_JSON_RESUME_ALLOW_DYNAMIC_THEME_INSTALL is explicitly enabled.
Deployments that need tighter supply-chain control should install from a
lockfile or equivalent pinned dependency set before owners preview or select
themes.
Theme Gallery And Previews¶
The JSON Resume website provides a visual theme gallery with screenshot images
and registry preview URLs. A 2026-06-23 snapshot found 61 gallery screenshot
slugs on https://jsonresume.org/themes. The npm registry keyword search for
jsonresume-theme returned 67 package hits, and django-resume’s stricter
package-name validation accepted 56 of them. These numbers are volatile and
should be treated as research snapshots rather than product invariants.
Registry demos should not be treated as exact previews for django-resume’s
catalog entries: they may use different resume data or a different installed
theme package version than the locally pinned catalog. The exact preview
surface is the owner-only Preview render action, which renders the current
resume with the catalog package version configured for this deployment.
The selector is gallery-first:
Display catalog themes as cards with a preview image, human-readable name, package/version, and short status such as installed, unavailable, or selected.
Start with preview image metadata derived from
jsonresume.org/themeswhere licensing and hotlinking/caching are acceptable. Do not make page rendering depend on the remote site being available; deployments should be able to cache or override preview image URLs.Later, generate django-resume-owned preview images from the installed catalog with a screenshot tool such as Simon Willison’s
shot-scraper. Generated previews should use a stable sample JSON Resume document and be refreshed by an operator/developer command, not on every page request.Previewing is separate from selecting. The preview action posts to a route whose path encodes the catalog key and renders without changing
Resume.integration_data. A separate Use render theme action persists the chosen theme underResume.integration_data["json_resume"]["theme"]. Persisting a selected theme is useful for “open rendered page” links, but it is not required just to inspect a theme.The persistence action uses explicit Use render theme wording. A card click does not both install executable code and persist selection.
Visual parity comparison against registry.jsonresume.org should harden the
theme-rendering work, but it should start as an explicit optional harness rather
than normal regression coverage. The harness should:
Fetch or vendor the official JSON Resume example document used by the JSON Resume site.
Import it into the example app and immediately export the portable projection so unsupported-field losses are visible in the report.
Render that projection locally with each enabled catalog render theme through the same
resumedpath used by the web view.Capture screenshots for the upstream registry render and the local render at fixed desktop and print-like viewports.
Compare screenshots with a perceptual threshold and write an HTML/PNG diff report for review. If a theme emits browser-side JavaScript, parity checks that compare browser output to standalone theme HTML should enable
DJANGO_RESUME_JSON_RESUME_ALLOW_THEME_SCRIPTSor account for the default CSP blocking runtime scripts.
The first useful gate should be “reviewable and reproducible” rather than “pixel-identical”. Expected differences may come from theme package versions, registry renderer behavior, network images, font availability, CSP/media handling, or django-resume import/export mappings. Once catalog versions, renderer versions, fonts, and fixture data are pinned tightly enough, selected themes can graduate to CI coverage with theme-specific thresholds.
Rendering should use the portable projection by default. Until the internal
export_resume() helper has first-class export intents, the renderer should
derive that projection by deep-copying the exported document and removing
meta.django_resume before passing it to external theme tooling. A private
round-trip render mode may pass the complete document only for owner-run
workflows, and reports should warn that theme packages may read django-resume’s
private extension envelope even if they usually ignore unknown fields.
The subprocess contract should be conservative:
Invoke the renderer without a shell and pass resume/theme/output paths as arguments.
Use a temporary working directory and input/output files created with owner-only permissions, such as
0700for directories and0600for files, clean up intermediate files, and avoid putting resume JSON in command-line arguments where it may appear in process listings.Run the child process with a minimal environment and a deterministic
PATH; CPU and memory limits beyond the timeout may be deployment-specific, but the renderer contract should not require passing application secrets to Node.Enforce a timeout and maximum stdout/stderr/output-file size.
Capture renderer stderr in the report, bounded and redacted where possible.
Fail closed if the export report is invalid, the theme key is unknown, the renderer exits non-zero, or the output file is absent or too large.
Do not fetch schemas, renderers, or themes from the network during rendering. The owner-only search and install flow may contact the npm registry, but the render step must use already-installed local packages.
HTML output should be treated as renderer-owned. The management command may
write the HTML file verbatim because the caller is running trusted local code.
HTTP preview/render responses use restrictive content security policy, private
caching, and no authenticated django-resume UI chrome inside the rendered theme
document. A future public or embedded preview should add iframe sandboxing.
Download responses should use owner-only authorization and may prefer
Content-Disposition: attachment to avoid executing third-party theme scripts
in the django-resume origin.
PDF output should be optional. The spec should not make Puppeteer, Playwright,
Chrome, or wkhtmltopdf a core Python dependency. A renderer definition may
advertise pdf support only when the deployment provides the required Node
or browser tooling.
Plugin-Based Approach¶
A single dedicated content plugin is not an obvious fit for whole-resume JSON
Resume import and export. Content plugins each own one section’s facts via
get_data(), while JSON Resume aggregates fields across many plugins such as
identity, work, education, skills, and projects. The work splits into two
layers:
Per-plugin mapping. Each content plugin knows how its own fields map to JSON Resume, so the mapping belongs in a plugin-owned adapter exposed by the plugin. Each adapter should declare the JSON Resume source paths it consumes and exports so the coordinator can apply update and
clear_missingrules at the plugin-ownership boundary even when a plugin spans multiple sections. Source paths should use JSON Pointer syntax, such as/basics/nameor/work. For the initial implementation, array-valued sections are owned as whole arrays, not individual item paths. Write ownership of source paths should be unambiguous: identical paths or prefix-overlapping paths are an import/export configuration error unless one adapter owns the parent path and coordinates the combined output. Shared write ownership is otherwise out of scope. An adapter that owns an array-valued path also controls item-levelx-django-resumefields for items it emits in that array. If a parent adapter coordinates child output, child plugins should contribute through that parent adapter’s explicit API rather than claiming overlapping JSON Resume write paths themselves. The coordinator should detect overlapping write path claims before each import or export operation and fail the operation with a configuration report instead of skipping plugins silently. The JSON Resume adapter protocol is part of the core import/export framework and should not depend on one aggregator that hard-codes every section. The structured-data API described in Application Workflows And Agentic Outreach should be the lower-level plugin facts API where available; JSON Resume adapters are format-specific mappings that may consume those facts. Maintaining separate independent extraction APIs for JSON Resume and application workflows would create duplication risk. The initial JSON Resume implementation should not block on the full application-workflows API, but it should define the smallest shared plugin facts primitives needed by the adapters and leave those primitives usable by the later structured-data API.Aggregation and transport. Assembling the document, validating it, exposing export and import endpoints, and routing an imported document back to the owning plugins is a cross-cutting concern. The coordinator should discover participating plugin adapters through the existing plugin registry. Core should expose JSON Resume import/export entry points, such as views or management commands, so plugins do not carry whole-document coordination or transport logic.
Export maps more cleanly than import: get_data() is per-section, so
importing a full document needs a coordinator that creates a resume and fans
each section out to the owning plugin. Plugins without a JSON Resume equivalent
should preserve unsupported data through the namespaced extension envelope when
round-trip fidelity matters.
The coordinator owns fallback behavior for plugins that do not provide JSON
Resume adapters, but fallback still requires plugin-side opt-in. On export, the
coordinator may store fallback plugin data under meta.django_resume.plugins
only for plugins that declare their fallback data safe for sharing or provide a
redacted fallback payload. Adapterless plugins without that declaration are
omitted by default and reported to the caller. On import, registered plugins
with restorable fallback data can receive that data back into their plugin
payloads only for trusted target-matching exports after
fallback-specific validation succeeds. Untrusted fallback restoration requires
explicit caller confirmation. Export-only redacted payloads and fallback data
without a validator should remain in the extension envelope and be reported
instead of being written into plugin storage. Data for unavailable plugins should
remain in the extension envelope and be reported to the caller.
A plugin that opts into fallback export must declare the fallback contract in its JSON Resume adapter: whether the payload is restorable or export-only, whether it is full-fidelity or redacted, the schema or validator used to validate it on import, and the redaction code used for export-safe partial payloads. The plugin owns that validator and must reject payloads that do not match the plugin version, expected shape, allowed field set, or redaction/restoration contract. The coordinator should call the validator before writing fallback data into plugin storage and should leave invalid, unavailable, export-only, or unvalidated payloads in the extension envelope with a report entry.
For update and clear_missing purposes, the source path for a fallback
payload should use JSON Pointer syntax, such as
/meta/django_resume/plugins/<plugin_name>. clear_missing may clear
plugin data for absent fallback paths only when clear_missing_fallbacks is
enabled and that plugin’s fallback participation is restorable; export-only or
redacted fallback declarations must not cause stored plugin data to be cleared.
Integration State¶
Resume.plugin_data should remain the home for plugin-owned resume content.
JSON Resume coordinator state is not plugin data, so it should live in a
dedicated core-owned JSON field such as Resume.integration_data. The JSON
Resume coordinator should own the integration_data["json_resume"] namespace.
Adding this field requires a migration with default=dict for existing
rows, and render-only resume querysets should explicitly defer the field when
large integration payloads are not needed.
Store only integration state there:
preserved third-party extensions that should be available for later re-export;
unrestored fallback payloads for unavailable plugins or untrusted documents;
provenance metadata needed for trusted same-installation round trips;
import/export reports and source-path diagnostics that help users understand omitted, ignored, or preserved fields.
The JSON document envelope should map directly into this storage namespace:
meta.django_resume.preserved_extensions maps to
integration_data["json_resume"]["preserved_extensions"],
meta.django_resume.plugins maps to
integration_data["json_resume"]["plugins"] for unrestored fallback payloads,
and provenance or report data should use sibling keys under
integration_data["json_resume"].
preserved_extensions should be stored as a list of records, not as an object
keyed only by source path, so collisions can keep multiple values. Each record
should include at least the source path, payload, origin classification, and
diagnostic metadata needed for reports and future re-export.
Do not duplicate normal resume facts there. Identity, work, education, skills,
projects, and other displayable resume content should stay in plugin-owned data.
This keeps normal CV and resume rendering aligned with the existing
plugin_data design. Adding a same-row JSONField does not add an extra
database query for normal page views, and large integration payloads can be
deferred from querysets that only render resume pages.
Custom Data¶
The JSON Resume schema version pinned for implementation determines which
extension points can be validated by the upstream schema. meta.django_resume
is the django-resume extension envelope and may be emitted for django-resume
round-tripping even when only the portable projection can be validated against
the upstream schema. Item-level x-django-resume extension fields should be
used only where the pinned schema allows them in a complete-document validation.
If the pinned schema does not allow a planned extension point, django-resume
should still validate a portable projection against the upstream schema and
validate the extension envelope with django-resume-specific rules. This should
be treated as a compatibility mechanism, not as a portable resume contract,
because third-party JSON Resume renderers and editors may ignore or reject
fields they do not understand.
Before creating a portable projection, import should use the pinned schema as an
allowlist to identify unknown third-party extension fields and move them into
meta.django_resume.preserved_extensions on the complete document when doing
so does not change standard data. This lifting step must exclude django-resume’s
own namespaces, including meta.django_resume and x-django-resume fields.
Untrusted django-resume extension fields are handled by the separate trust-gated
preservation step described in the validation section.
The allowlist should be based on the pinned schema’s explicitly defined standard
properties, not only on whether validation permits additional properties. If the
schema’s permissiveness prevents reliable classification, the import report
should say which extension fields could not be confidently lifted.
A portable projection is then derived from the complete document by removing
django-resume-specific extension data such as meta.django_resume and all
x-django-resume item-level fields. It is the
baseline artifact used to check compatibility with the pinned upstream schema.
The complete exported document may still include the extension envelope for
django-resume round-tripping; reports should distinguish portable-projection
validation from complete-document extension validation.
A trusted django-resume-originated export is a document with django-resume
provenance metadata and a signature generated with Django’s signing framework
for the current installation. The signed payload should cover a canonical digest
of the complete exported document using a fixed JSON canonicalization scheme
such as RFC 8785, excluding only the signature field itself. Provenance fields
such as owner, resume slug, and export timestamp should live under
meta.django_resume.provenance and are covered by that digest. The signature
should live at a fixed path such as meta.django_resume.signature. Signature
verification should run on the received document before any unknown-field
lifting, normalization, or projection. An unsigned, content-modified, or
unverifiable meta.django_resume object should be treated as untrusted
extension data subject to the gated import rules below, not as proof that
destructive update defaults are safe.
This ordering still allows the canonicalization needed to compute the signed
digest; the prohibited normalization is django-resume data normalization, such
as date or URL normalization, and unknown-field lifting before verification.
For update, a trusted target-matching export is a trusted
django-resume-originated export whose signed owner and slug match the selected
target resume and pass the trusted-round-trip freshness policy. Destructive
defaults should use this stricter condition, not signature validity alone.
For create, the corresponding signed same-identity restoration condition is
defined separately because there is no existing target resume whose freshness can
be checked.
Trusted destructive defaults are limited to behaviors whose source paths are
present in a trusted target-matching export: applying present-empty standard
sections, replacing stored preserved_extensions from a present envelope
payload, and restoring present restorable fallback payloads, including empty
fallback payloads. clear_missing is never a trusted destructive default
because it acts on absent paths.
This trust model is installation-local. Exports from another django-resume
installation, exports from the same installation after signing-key rotation, or
exports that pass through tools that change document content should be treated
as untrusted JSON Resume documents and use the conservative update defaults
unless the caller explicitly confirms destructive options. Pure serialization
changes that preserve the canonical digest do not by themselves break trust.
Because Django signing is normally anchored in SECRET_KEY and configured
fallback signing keys, trusted round trips inherit that operational boundary:
load-balanced instances sharing a signing key are one trust domain, while key
rotation can make older exports untrusted unless the deferred key-rotation
policy explicitly accepts them.
Trusted round trips must already contain preserved extensions in the
meta.django_resume envelope at export time; signature verification happens
before any import-time lifting of unknown fields.
A valid signature proves origin and integrity, not freshness. The signed export
timestamp should be reported to the caller and used with stored JSON Resume
integration state, such as the last accepted signed import or a target version
marker, to prevent stale same-installation exports from silently rolling back
newer data. If freshness cannot be evaluated, imports should fall back to
untrusted-document defaults, where destructive behavior requires explicit
confirmation.
django-resume should expose two export intents from the initial implementation.
A django-resume round-trip export should serialize the complete extended
document, including meta.django_resume extension data and export-safe
fallback payloads, plus item-level x-django-resume fields when the pinned
schema permits them. It is unsigned until the canonicalization/signature phase
is implemented; after that phase, the same intent should include provenance
metadata and a signature by default so same-installation imports can identify
trusted target-matching round trips. A portable download intended for external
sharing should serialize the portable projection with django-resume extension
data stripped, and should default to unsigned.
Before the signature phase lands, all imports use untrusted-document defaults.
Unsigned exports should always import with untrusted-document defaults.
Signed exports include owner, slug, timestamp, and a signature that can reveal
internal provenance or enable correlation across exports. User-facing download
flows should choose the portable unsigned intent unless the user explicitly asks
for a same-installation round-trip export.
Use these rules for unsupported plugin data:
Map to standard JSON Resume fields first.
Use
meta.django_resumefor document-level django-resume metadata and versioning, including source plugin metadata when needed.Use
meta.django_resume.pluginsfor django-resume-owned fallback plugin payloads that are explicitly safe to export. Fallback payloads must state whether they are restorable. Redacted or partial fallback payloads should be marked export-only and must not overwrite full plugin data on import.Use
meta.django_resume.preserved_extensionsfor unknown third-party extension fields when they cannot be mapped into standard fields or plugin adapters. Preserve source path metadata for diagnostics and best-effort re-export insidemeta.django_resume.preserved_extensions, but do not promise restoration to the original item location until adapters define stable item identity.Use
x-django-resumeas the item-level extension key only when the data belongs to a specific exported item, the owning adapter controls that item, and the pinned schema allows additional properties for that item type. Deferred trusted round trips may consume these fields as part of replacing the owning adapter’s whole array or section. Restoring unknown item-level extension data into an existing item without whole-array replacement is future work and requires stable item identity.Keep page layout, permissions, token state, workflow state, and delivery history out of JSON Resume exports. Those belong to application-private data formats, not the JSON Resume interchange document.
Plugins that provide no JSON Resume adapter and no explicit export-safe fallback declaration should be omitted from export by default, with a report entry explaining that their data was not included.
Validation¶
Use layered validation:
Validate a portable projection of exported and imported documents against a pinned upstream JSON Resume schema to check compatibility with the supported external format version.
Validate the complete extended document against the pinned upstream schema when the pinned schema allows the extension points used by the document.
Validate django-resume extensions and plugin adapter requirements separately.
This keeps the compatibility contract clear while avoiding a live network dependency and leaving room for django-resume-specific import checks, conflict handling, and extension-envelope validation.
Import should follow this order:
Enforce input size limits before parsing.
Parse JSON with guarded nesting-depth limits. Reject duplicate keys in all phases. Non-finite numbers or other values that cannot be represented in the pinned JSON Resume schema should fail validation; canonicalization-specific rejection rules apply once the provenance signature phase is implemented.
If canonicalization and signing are implemented, verify django-resume provenance on the received document before any normalization, unknown-field lifting, or projection. Otherwise, treat the document as untrusted.
Use the pinned schema as an allowlist to lift unknown third-party extension fields into
meta.django_resume.preserved_extensionswhen that can be done without changing standard data.For documents that are not trusted target-matching exports, preserve inbound
x-django-resumefields inmeta.django_resume.preserved_extensionswith a report entry before building the portable projection.For trusted target-matching exports, let registered adapters consume inbound
x-django-resumefields for source paths they own before building the portable projection. Preserve and report trustedx-django-resumefields with no owning adapter. This branch is inert until the signature and freshness phase makes trusted target-matching exports possible, and initial consumption is limited to whole-array or whole-section replacement by the owning adapter.Build the portable projection and validate it against the pinned schema.
Validate the post-lifting complete document against the pinned upstream schema when the pinned schema allows the extension points used by the document.
Run django-resume extension and adapter validation.
Apply
createorupdatewith the conservative destructive-option defaults described below.
If export cannot produce a portable projection that validates against the pinned schema, export should fail with a validation report. It should not silently strip or rewrite invalid fields to force a schema-valid document. The initial implementation should require a schema-valid portable projection for JSON Resume export; draft or lenient export modes can be considered later as separate features. If the pinned schema permits the extension points used by a complete extended export but the complete document fails upstream validation, the round-trip export should also fail with a validation report. It should not silently fall back to portable-only validation when complete-document validation is expected.
JSON Resume does not define a reliable field for selecting the schema revision.
Do not treat meta.version as the schema version. Import should validate the
portable projection against django-resume’s pinned supported schema. If the
document includes an explicit schema URI or canonical pointer that differs from
the pinned schema, the import report should warn that structural validation does
not prove semantic compatibility across schema revisions.
If an imported document’s portable projection fails validation against the pinned
schema, import should fail with a validation report. Unknown third-party
extension fields should be lifted into preserved_extensions before this
validation step when they can be identified without changing standard data.
For update of documents that are not trusted target-matching exports, newly
lifted preserved extensions should be merged into the stored
preserved_extensions set without dropping existing entries. Merge precedence
should be explicit: exact duplicates with the same content and source-path
metadata are deduplicated; non-item-level entries with the same source path and
different content are collisions and should both be kept and reported unless
replace_preserved_extensions is enabled; non-colliding entries are appended
or merged by source path as appropriate for the storage representation. Item-level
entries should include source path metadata for diagnostics but should not use
array-index paths as stable identity until stable item identity is defined. For
item-level entries, array-index source paths may be used only to detect exact
duplicate diagnostic records, not to restore data into an existing item or merge
different payloads. Replacing or clearing existing preserved extensions still
requires replace_preserved_extensions.
This merge rule applies to unknown third-party fields lifted from their original
document locations. It does not grant trust to an inbound
meta.django_resume.preserved_extensions payload, which is handled by the
separate trust-gated rule below.
For trusted target-matching exports, import-time lifting of unknown third-party
fields should normally be a no-op because preserved extensions must already be
inside the signed meta.django_resume envelope. If trusted input still
contains stray unknown fields outside that envelope, preserve and report them
using the same merge, deduplication, and bound rules as untrusted lifted fields
rather than silently dropping them.
Stored preserved extensions should be deduplicated by content and source-path
metadata where possible, and bounded by a configured count or byte-size limit so
repeated imports cannot grow integration_data without limit. When the bound
would be exceeded, import should reject the import or require an explicit
replacement option rather than silently truncating data or dropping existing
entries. The same bound applies to trusted whole-set replacement from
meta.django_resume.preserved_extensions; trusted payloads that exceed the
bound should fail or require explicit replacement handling, not bypass the limit.
Inbound x-django-resume fields from documents that are not trusted
target-matching exports should be handled by the trust-gated preservation step
above, not silently dropped or applied as trusted django-resume item data.
For trusted target-matching exports, the owning adapter may consume
x-django-resume fields for source paths it owns. Trusted x-django-resume
fields that no registered adapter owns should be preserved and reported rather
than silently dropped.
Until the signature and freshness phase lands, item-level x-django-resume
support is limited to export, validation, and preservation on import; consuming
those fields back into plugin storage belongs to the deferred trusted-round-trip
phase.
If an untrusted document already contains a non-empty
meta.django_resume.preserved_extensions payload and
replace_preserved_extensions is not enabled, that payload should remain in
the extension envelope and be reported as not imported into stored preserved
extensions.
If a standard JSON Resume source path is present on import and no registered
adapter owns that path, the coordinator should preserve that data in
meta.django_resume.preserved_extensions when possible and report that no
plugin imported it. It should not silently drop unclaimed standard sections.
For import updates, a JSON Resume path is present when its property exists in
the imported document, even if the value is empty. For trusted target-matching
exports, present empty arrays or objects replace the corresponding plugin-owned
data with empty data. Absent source paths leave existing data unchanged by
default, or clear existing data only when clear_missing is enabled.
For object-valued sections, an empty object means exactly {}; objects whose
fields are present but empty are handled by the owning adapter’s validation and
normalization rules rather than by the empty-object shortcut.
If adapters own sub-paths under an empty parent object such as /basics,
trusted target-matching imports should treat the empty parent as present-empty
for those declared child paths. The coordinator should detect the empty parent,
fan it out to the adapters that declare owned child source paths, and preserve
or report unowned child paths according to the normal unclaimed-path rules. For
documents that are not trusted target-matching exports, the empty parent object
should be a no-op by default unless the caller enables
apply_empty_sections.
Signed django-resume exports intended for trusted round trips should emit owned
empty standard sections explicitly so deletions can round-trip. Unsigned
portable exports may omit empty fields for readability. Unsigned django-resume
round-trip exports should prefer the same explicit empty-section shape as future
signed round-trip exports for forward compatibility, but they still import with
untrusted-document defaults until signing and freshness exist. Omitted fields
import as absent paths.
Present non-empty standard sections from documents that are not trusted
target-matching exports should update the owning plugin data after validation;
the conservative defaults only change destructive empty, missing, fallback, and
preserved-extension behavior.
JSON null values are present values, not absent paths. They should be handled
by the owning adapter if the pinned schema and plugin mapping allow null;
otherwise they should fail validation rather than being treated as empty data.
Deterministic normalization is allowed when converting stored django-resume data to JSON Resume representation, such as formatting dates or URLs according to the pinned schema. Normalization should not change the meaning of a value merely to make validation pass; values that cannot be represented validly without changing their meaning should cause export or import validation to fail with a report.
clear_missing is a destructive update option. It should require explicit
confirmation and per-section or per-plugin scope for all imports, including
trusted target-matching exports, because missing paths are not trusted
destructive defaults.
For documents that are not trusted target-matching exports, present empty standard sections should be treated as no-op updates by default and reported, unless the caller explicitly opts into applying empty sections. This keeps third-party exports that include empty arrays from clearing existing plugin data accidentally.
Imported meta.django_resume.preserved_extensions should replace the stored
preserved-extension set only for trusted target-matching exports or when
replace_preserved_extensions is enabled. If the key is absent during
update, existing preserved extensions remain unchanged. clear_missing
does not affect preserved-extension handling. An explicitly empty
preserved_extensions value clears the stored set through the same
replace-on-present rule. The import report should mention when previously stored
preserved extensions are dropped this way.
Fallback plugin restoration from meta.django_resume.plugins should also be
trusted-target-matching only by default during update. For documents that
are not trusted target-matching exports, fallback payloads should remain in the
extension envelope and be reported unless the caller explicitly confirms that
restorable fallback data from the document should be written into plugin
storage. Present empty fallback payloads should clear plugin data only for
trusted target-matching exports or when the caller explicitly enables both
restore_untrusted_fallbacks and apply_empty_fallbacks.
For create, standard mapped JSON Resume data can populate the new resume
after validation. A signed export can restore fallback plugin payloads during
create only when its signed owner and slug match the caller-supplied owner
and slug for the new resume, the globally unique slug is not already in use, and
the import is effectively restoring a previously exported resume into an empty
slot, such as after deletion. This signed same-identity restoration path belongs
to the deferred trusted-round-trip phase. Duplicating a signed export under a
different slug is not signed same-identity restoration; those fallback payloads
should remain in the extension envelope unless restore_untrusted_fallbacks
is enabled. Trusted provenance is not required for standard fields because no
existing plugin data is being overwritten.
Signed same-identity restoration during create should still report the
signed export timestamp and require explicit confirmation that the caller wants
to restore fallback payloads for that signed owner and slug. Slug reuse means an
old signed export could otherwise populate fallback data for a newly intended
resume that happens to reuse the same slug.
Unknown third-party extensions and preserved django-resume extension fields
encountered during create should be stored under
integration_data["json_resume"] and reported, following the same preservation
rules as update.
Until the provenance signature phase is implemented, the initial import/export
implementation should use untrusted-document defaults for all imports. That
means trusted deletion round-trips and automatic fallback restoration without
restore_untrusted_fallbacks are not available in the initial phase.
Explicit restore_untrusted_fallbacks remains available for callers that
deliberately opt into fallback restoration. clear_missing is always
confirmed and scoped, regardless of phase or trust status.
Destructive import options should be exposed separately so callers can make deliberate choices:
These options apply to update unless explicitly stated otherwise. create
has no existing plugin data to clear, so clearing options such as
clear_missing, apply_empty_sections, and apply_empty_fallbacks are
inert there. Fallback restoration options may still control whether fallback
payloads are written into the newly created resume.
clear_missingclears plugin data for absent adapter source paths and defaults toFalse. It requires a preview plus per-section or per-plugin confirmation for all imports.clear_missing_fallbacksallowsclear_missingto clear restorable fallback-backed plugin data for absent/meta/django_resume/plugins/<plugin_name>paths. It is a no-op unlessclear_missingis also enabled. It defaults toFalseand should warn that unrelated third-party imports normally omit those paths.apply_empty_sectionsapplies present empty standard sections from documents that are not trusted target-matching exports. It defaults toFalse.restore_untrusted_fallbackswrites restorable fallback plugin data from documents that are not trusted target-matching exports into plugin storage. It defaults toFalse. Until signing and freshness are implemented, this option also covers unsigned same-installation round-trip exports because they cannot yet prove trusted target-matching provenance. Forcreate, it controls whether restorable fallback payloads are written into the newly created resume when signed same-identity restoration is not available.apply_empty_fallbacksallows present empty fallback payloads to clear plugin data whenrestore_untrusted_fallbacksis also enabled. It defaults toFalse.replace_preserved_extensionsallows an empty or replacementpreserved_extensionspayload from a document that is not a trusted target-matching export to drop stored preserved extensions. It defaults toFalse.Target-resume confirmation is always required for
updateand does not imply any of the destructive options above.
For non-interactive management commands, confirmation should require an explicit
flag for each destructive option after printing or writing the preview report.
A single generic --yes flag should not imply all destructive options.
Options that affect multiple sections or plugins, such as clear_missing,
should accept explicit path or plugin names so command callers can confirm the
same per-section or per-plugin scope required by interactive flows. Trusted
target-matching imports that pass freshness checks do not need per-path names
for trusted destructive defaults, but they still need target-resume confirmation
and an explicit flag for each destructive option that is not part of those
defaults. Because clear_missing is never a trusted destructive default, it
always requires explicit path or plugin names when enabled.
Preview reports for destructive imports should include a target version marker,
such as updated_at plus a checksum of adapter-owned source paths and the
integration_data["json_resume"] namespace. The apply step should run
transactionally and verify that the target state still matches the previewed
version before applying destructive changes, or regenerate the preview and
require confirmation again.
Operation Permissions¶
JSON Resume import and export operate on personal resume data and must use resume-level permission checks.
View-based export should require the same authorization as editing or owning the resume. Public resume pages and token-protected CV views should not imply permission to download a JSON Resume export.
View-based import should require permission to mutate the selected resume and should run inside a transaction.
Management commands should require an explicit target owner or resume and should not bypass model-level ownership checks silently.
Import/export reports should include omitted plugins, unsupported fields, and schema-version warnings, but should avoid leaking redacted plugin data.
Import views and commands should enforce maximum input size and nesting-depth limits before schema validation and extension lifting.
Theme rendering should require the same permission as JSON Resume export. Management commands are operator-run, while web views should be authenticated and owner-only. The production-facing web flow should prefer curated, deployment-owned catalog keys that resolve to pinned installed packages. Any owner-triggered npm installation flow must be explicitly enabled, validate npm package names before installing them, and be documented as executing third-party JavaScript during rendering.
Core Boundary¶
JSON Resume support should not be an external package for the initial implementation. The generic import/export framework belongs in core, and JSON Resume should be the first bundled format implemented on top of that framework. This keeps JSON Resume easy to use while avoiding a design where JSON Resume becomes the only assumed interchange format or the internal django-resume data model.
Round-Trip Fidelity¶
Round-trip fidelity means how much information survives a conversion sequence such as exporting a django-resume resume to JSON Resume, importing that document back into django-resume, and exporting it again.
django-resume should not promise byte-for-byte identical output. Field order, empty field omission, generated IDs, normalized dates, defaults, Markdown normalization, and URL normalization may change across import and export. For signed django-resume round-trip exports, omit empty fields only when their absence should not clear existing data on update.
The expected fidelity is phase-aware:
Preserve supported standard JSON Resume fields semantically.
Preserve django-resume extension data under
meta.django_resumefor signed trusted round trips, or when the caller explicitly enables the relevant untrusted-document options such asrestore_untrusted_fallbacksorreplace_preserved_extensions. Initial unsigned imports do not apply inboundmeta.django_resume.preserved_extensionspayloads to stored preserved extensions by default; they should report and retain untrusted envelope data only according to the integration-state preservation rules above.Preserve unknown third-party extension fields on a best-effort basis when they do not conflict with mapped plugin data, using
meta.django_resume.preserved_extensionswith source path metadata when needed. Re-exporting unknown item-level fields at their original item location is not required until adapters define stable item identity.Omit adapterless plugins that provide no export-safe fallback declaration, and report that omission. Their data is not covered by round-trip fidelity guarantees.
Report unsupported, ignored, or dropped fields during import.
Remaining Work¶
Blocking tasks for initial implementation:
Resolve the schema-pinning open question by selecting the upstream schema version used for validation.
After schema pinning, compare schema fields against django-resume’s existing models and bundled plugins.
Define the minimal shared plugin facts primitives needed by JSON Resume adapters so the initial adapters do not diverge from the later structured-data API.
Define the fallback declaration and validator contract for export-safe, restorable, export-only, and redacted fallback payloads.
Identify lossy mappings in both directions.
Add validation tests covering
meta.django_resumeand schema-allowed item-level namespaced fields.Check whether existing JSON Resume tooling can be reused for validation or conversion tests.
Remaining JSON Resume theme-rendering work:
Decide whether a separate
render_json_resumemanagement command is still needed now that owner-only web rendering exists. Consider the command as an operator/CI surface rather than as a replacement for the curated gallery.Generate or cache django-resume-owned catalog preview images instead of depending on remote preview image URLs in the checked-in default catalog.
Build the optional visual parity harness described above. Keep the first version outside normal
just checkand use its report to find import/export and renderer-compatibility bugs before promoting stable theme cases to CI.Add PDF rendering support only after the deployment contract for Puppeteer, browser binaries, or another HTML-to-PDF tool is explicit.
Add stronger process isolation options for deployments that allow unreviewed community themes, such as containerized rendering or host-level memory and CPU limits.
Deferred trusted-round-trip phase:
Implement provenance signing and verification together for django-resume round-trip exports and imports.
Choose and test the JSON canonicalization implementation used for provenance signatures, including canonicalization-specific duplicate-key behavior, non-finite number handling, and values whose representation may change under canonicalization. General duplicate-key rejection is required from the initial import phase.
Define trusted import freshness and replay protection, such as monotonic accepted export timestamps, signed export identifiers, or target version checks before enabling trusted destructive defaults.
Define signing-key rotation behavior for trusted provenance.
Future work:
Define stable item identity for future item-level extension restoration and merge-by-item updates.
Implementation Sketch¶
The current unsigned implementation supports management-command export and create-only import.
export_json_resume emits standard JSON Resume fields through plugin-owned
export adapters and also writes a django-resume extension envelope at
meta.django_resume. The envelope currently includes
meta.django_resume.plugin_data so same-application imports can recreate a
fresh resume without losing django-resume-only fields such as display dates,
section titles, local image paths, or adapterless plugin data. The standard
projection remains valid against the vendored JSON Resume v1.0.0 schema; values
that do not fit portable fields are reported and kept in the extension envelope.
Because the envelope currently contains application-private plugin payloads,
including access-control data when token plugins are configured, these exports
are private backup or migration files rather than public registry uploads unless
the envelope is removed.
This also means the raw JSON download is not the ideal input for external JSON
Resume themes. Users can still render it with tools that ignore unknown
meta fields, but django-resume’s theme-rendered route derives a portable
projection and strips meta.django_resume before invoking the Node renderer.
The same export is available to authenticated resume owners through the
non-page <slug>/json-resume/ route, which is ordered before the page
registry catch-all.
import_json_resume creates a new resume for a caller-supplied owner and slug.
Authenticated owners can run the same create-only import from the resume
overview by uploading a JSON Resume file, or by providing a public HTTP(S) JSON
Resume URL, plus a new slug; the browser flow always assigns ownership to the
current user and reports the import result inline. By default, if
meta.django_resume.plugin_data is present, that
payload is restored into the new resume. Passing --portable-only in the
command, or selecting Standard JSON Resume fields only in the browser,
ignores the envelope and maps standard JSON Resume sections through plugin-owned
import adapters.
The restored plugin-data envelope is validated before writing: it must be an
object whose values are plugin payload objects. Import adapter source paths are
checked for duplicate or overlapping claims before any adapter data is applied.
If meta.django_resume.preserved_extensions is present, it is stored under
integration_data["json_resume"]["preserved_extensions"] and emitted again
on later JSON Resume exports.
All successful imports also store the parsed source document, the
adapter-generated source projection, and the imported plugin-data snapshot under
integration_data["json_resume"]. If a later export’s current adapter
projection and full plugin data still match those stored values, export re-emits
the parsed source document exactly. This gives third-party JSON Resume samples
an exact unchanged import/render/export path without hiding later plugin-data
edits; once plugin data changes, export regenerates the document from adapters
and writes the django-resume extension envelope.
The current portable /work import maps all entries into
employed_timeline and reports that JSON Resume cannot recover the original
freelance/employed timeline split.
Portable import also reports currently unsupported standard fields such as
additional education entries, unmapped profile networks, basics.url, skill
levels or keywords, and defaulted section titles.
JSON Resume render theme support currently lets authenticated owners browse a
curated catalog, and optionally search npm for jsonresume-theme-* and
@jsonresume/jsonresume-theme-* packages when dynamic discovery is enabled.
It installs the selected render theme plus resumed into a local cache,
stores that package name or catalog key under
integration_data["json_resume"]["theme"], and opens a private rendered HTML
page. Rendering derives a portable projection with
meta.django_resume stripped before passing data to the Node renderer.
Update-in-place imports, signatures, freshness checks, trusted destructive
defaults, and PDF rendering remain deferred.