========================
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 :ref:`jsonresume-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_resume`` for document-level metadata,
``meta.django_resume.plugins`` for fallback plugin payloads, and
``meta.django_resume.preserved_extensions`` for preserved unknown extensions.
Conditional on the pinned schema allowing the item type, use item-level
``x-django-resume`` extension 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_data`` because that field is plugin-owned. Add a dedicated
core-owned JSON field such as ``Resume.integration_data`` and store JSON Resume
envelope state under ``integration_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.version`` must 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 validate ``meta.django_resume`` and 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_resume`` extension 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 ``create`` and ``update`` modes. ``create`` creates a new
resume from JSON Resume data for a caller-supplied owner and
caller-supplied ``Resume.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. ``create`` should fail if
that globally unique slug already exists. Overwrite-on-create is out of scope
for the initial design. ``update`` updates 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 both ``restore_untrusted_fallbacks`` and
``apply_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 unless
``clear_missing`` is enabled. Participating plugins with absent source paths
should be left unchanged by default. A separate ``clear_missing`` option may clear
participating plugin data whose adapter-owned standard source paths are absent
from the imported document, but it should default to ``False`` and should
always require a preview plus per-section or per-plugin confirmation because
missing paths are not trusted destructive defaults. ``clear_missing`` does not
apply to ``meta.django_resume.preserved_extensions``; preserved-extension
replacement is controlled by ``replace_preserved_extensions``.
``clear_missing`` applies to fallback-backed plugin data only when
``clear_missing_fallbacks`` is 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 ``/`` route; the page registry owns that
ordering.
See :doc:`application_workflows` and :doc:`page_plugins`.
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 ``theme`` plugin 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 under
``integration_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 ``//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 ``//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.html`` in 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_resume`` management
command that exports a resume, derives a portable projection, writes it to a
temporary file, and invokes a configured local renderer such as
``node_modules/.bin/resumed``. The command would accept an allowlisted theme
key and an output format such as ``html`` or ``pdf``. 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 ``resumed`` workflow 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/themes`` where
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 under ``Resume.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 ``resumed`` path 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_SCRIPTS`` or 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 ``0700`` for directories and ``0600`` for
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_missing`` rules at
the plugin-ownership boundary even when a plugin spans multiple sections.
Source paths should use JSON Pointer syntax, such as ``/basics/name`` or
``/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-level ``x-django-resume``
fields 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
:doc:`application_workflows` 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/``. ``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.
.. _jsonresume-custom-data:
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_resume`` for document-level django-resume metadata and
versioning, including source plugin metadata when needed.
* Use ``meta.django_resume.plugins`` for 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_extensions`` for 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 inside ``meta.django_resume.preserved_extensions``, but do not
promise restoration to the original item location until adapters define stable
item identity.
* Use ``x-django-resume`` as 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_extensions`` when that can be done
without changing standard data.
* For documents that are not trusted target-matching exports, preserve inbound
``x-django-resume`` fields in ``meta.django_resume.preserved_extensions`` with
a report entry before building the portable projection.
* For trusted target-matching exports, let registered adapters consume inbound
``x-django-resume`` fields for source paths they own before building the
portable projection. Preserve and report trusted ``x-django-resume`` fields
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 ``create`` or ``update`` with 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_missing`` clears plugin data for absent adapter source paths and
defaults to ``False``. It requires a preview plus per-section or per-plugin
confirmation for all imports.
* ``clear_missing_fallbacks`` allows ``clear_missing`` to clear restorable
fallback-backed plugin data for absent
``/meta/django_resume/plugins/`` paths. It is a no-op unless
``clear_missing`` is also enabled. It defaults to
``False`` and should warn that unrelated third-party imports normally omit
those paths.
* ``apply_empty_sections`` applies present empty standard sections from
documents that are not trusted target-matching exports. It defaults to
``False``.
* ``restore_untrusted_fallbacks`` writes restorable fallback plugin data from
documents that are not trusted target-matching exports into plugin storage. It
defaults to ``False``. 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. For ``create``, it
controls whether restorable fallback payloads are written into the newly
created resume when signed same-identity restoration is not available.
* ``apply_empty_fallbacks`` allows present empty fallback payloads to clear
plugin data when ``restore_untrusted_fallbacks`` is also enabled. It
defaults to ``False``.
* ``replace_preserved_extensions`` allows an empty or replacement
``preserved_extensions`` payload from a document that is not a trusted
target-matching export to drop stored preserved extensions. It defaults to
``False``.
* Target-resume confirmation is always required for ``update`` and 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_resume`` for signed
trusted round trips, or when the caller explicitly enables the relevant
untrusted-document options such as ``restore_untrusted_fallbacks`` or
``replace_preserved_extensions``. Initial unsigned imports do not apply
inbound ``meta.django_resume.preserved_extensions`` payloads 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_extensions`` with 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_resume`` and 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_resume`` management 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 check`` and 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 ``/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.