JSON Schema reference
The schemas/ directory holds JSON Schema (draft 2020-12) files generated
from the package’s Pydantic models in researcher_profiles.schema. They exist so
a consumer can validate a profile directory without installing this package,
in any language with a draft-2020-12 validator.
Each file corresponds to one on-disk artifact of a profile directory.
| File | Validates (on-disk path) | Source model | Root type | additionalProperties |
|---|---|---|---|---|
profile_metadata.schema.json | profile.yaml | ProfileMetadata | object + $defs | allowed (tolerant) |
profile_config.schema.json | config.json | ProfileConfig | object | allowed (tolerant) |
papers_file.schema.json | sources/papers.yaml | PapersFile (wraps PaperRecord via $defs) | object + $defs | allowed on PaperRecord (tolerant) |
grants_file.schema.json | sources/grants.yaml (deep profiles) | GrantsFile (wraps GrantRecord via $defs) | object + $defs | allowed on GrantRecord (tolerant) |
summary_file.schema.json | frontmatter of sources/summaries/*.summary.md | SummaryFile | object | forbidden (strict) |
question.schema.json | entries of meta/questions.yaml | Question | object | forbidden (strict) |
“Tolerant” models (profile_metadata, profile_config, PaperRecord) set
additionalProperties: true — unknown keys validate. Strict models
(summary_file, question) set additionalProperties: false — unknown keys are
a validation error.
Optional fields render in the schema as anyOf [ {type}, {"type": "null"} ];
this reference writes that as type | null.
Regeneration workflow and invariant
Section titled “Regeneration workflow and invariant”The checked-in schemas/*.json files are generated artifacts. Regenerate
them from the models with the CLI:
researcher-profiles schema export schemas/or programmatically:
from researcher_profiles.schema_export import export_schemas, build_schemas
export_schemas("schemas") # writes <name>.schema.json filesbuild_schemas() # returns {name: schema_dict} without writingInvariant: the checked-in schemas/*.json must be regenerated whenever a
model in researcher_profiles.schema changes. A schema file that disagrees
with its model is a bug — the models are authoritative.
Output format (so a reviewer can eyeball a diff): one <name>.schema.json per
model, JSON with sorted keys, 2-space indent, and a trailing
newline.
This is a maintainer convention: run the export command after editing a model and commit the regenerated files. There is currently no automated test that compares the checked-in bytes to a fresh export (see Test coverage below).
See the Export JSON Schemas how-to for the step-by-step.
Validation without installing the package
Section titled “Validation without installing the package”This is the load-bearing consumer recipe. Both paths use only third-party tools —
no researcher-profiles install.
(a) Python with the third-party jsonschema package
Section titled “(a) Python with the third-party jsonschema package”import json, yamlfrom jsonschema import Draft202012Validator
schema = json.load(open("schemas/profile_metadata.schema.json"))data = yaml.safe_load(open("jane-doe/profile.yaml")) # YAML -> JSON-compatible dictDraft202012Validator(schema).validate(data) # raises on mismatchNotes:
- Parse YAML first.
profile.yaml,sources/papers.yaml,meta/questions.yaml, and summary frontmatter are YAML — load them to JSON-compatible data withyaml.safe_loadbefore validating.config.jsonis already JSON (json.load). $defsresolve internally. Thepapers_fileandprofile_metadataschemas contain$defs; a compliant draft-2020-12 validator resolves these from the single file — no extra registry or reference wiring is needed.- Pin the dialect. Use
Draft202012Validatorexplicitly rather thanvalidate(...)auto-detection.
(b) check-jsonschema CLI (no Python authoring)
Section titled “(b) check-jsonschema CLI (no Python authoring)”check-jsonschema --schemafile schemas/profile_metadata.schema.json jane-doe/profile.yamlcheck-jsonschema parses YAML natively, so the same command works for the YAML
artifacts and for config.json. Each per-schema section below refers to “the
recipe above” plus its own schema/instance pair.
The level field
Section titled “The level field”level is a string enum, present on both profile_config and
profile_metadata:
- Type:
string - Allowed values:
"lite","full","deep" - Default:
"full" - An absent
levelvalidates and is treated asfull, so profiles written before this field existed remain valid.
This reference documents the field’s type/enum/default only. For what the
lite/full/deep tiers mean and how they gate the build, see
Profile format.
The deep-input config fields
Section titled “The deep-input config fields”Four profile_config fields form the supplied-input contract that the deep
level is defined by — grants_source, reporter_supplement, cv_source, and
websites. They are documented in the
profile_config table below. Being config
fields they are always schema-optional: nothing in JSON Schema can express
“required when level is deep”. That precondition (“a deep build needs at least
one supplied source”) belongs to the producing pipeline, not to this format
spec — a deep profile whose sources are all absent is still a structurally valid
profile directory.
profile_metadata (profile.yaml)
Section titled “profile_metadata (profile.yaml)”Top-level profile metadata. Source model: ProfileMetadata (tolerant base —
extra keys allowed, string values whitespace-stripped). Only name is required.
| Field | Type | Required? | Default | Constraints / enum | Description |
|---|---|---|---|---|---|
name | string | yes | — | minLength: 1 | Researcher display name. |
level | string enum | no | "full" | lite | full | deep | Profile depth tier. See The level field. |
orcid | string | null | no | null | ORCID pattern ^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$ (in the JSON schema; the model additionally strips surrounding whitespace before matching) | ORCID identifier. |
affiliation | string | null | no | null | Institutional affiliation. | |
scholar_url | string | null | no | null | Google Scholar profile URL. | |
openalex_id | string | null | no | null | OpenAlex author ID. | |
field | string | null | no | null | Primary field. | |
subfields | list[string] | no | [] | Subfield labels. | |
summary | string | null | no | null | Prose overview. | |
training | list[Training] | no | [] | see Training sub-table | Education / training history. |
career | list[CareerEntry] | no | [] | see CareerEntry sub-table | Positions held. |
expertise | list[string] | no | [] | Expertise topic labels. | |
interests | list[string] | no | [] | Research interests. | |
not_interests | list[string] | no | [] | Explicit non-interests. | |
methodological_commitments | list[string] | no | [] | Methodological stances. | |
recurring_positions | list[string] | no | [] | Positions taken repeatedly. | |
intellectual_lineage | list[string] | no | [] | Intellectual influences. | |
critiques | list[string] | no | [] | Recurring critiques. | |
artifacts | list[Artifact] | no | [] | see Artifact sub-table | Software / datasets / other outputs. |
collaborators | list[string | object] | no | [] | each item is a string OR {name, affiliation, relationship} object | Collaborators. |
anchor | Anchor | null | no | null | see Anchor sub-table | Disambiguation evidence. |
paper_stats | PaperStats | null | no | null | see PaperStats sub-table | Author-position counts and year range. |
Nested $defs sub-objects
Section titled “Nested $defs sub-objects”Training (tolerant; degree and institution required):
| Field | Type | Required? | Default |
|---|---|---|---|
degree | string | yes | — |
institution | string | yes | — |
year | integer | string | null | no | null |
advisor | string | null | no | null |
CareerEntry (tolerant; all three required). years coerces int to
string at the model layer.
| Field | Type | Required? |
|---|---|---|
role | string | yes |
institution | string | yes |
years | string | yes |
Artifact (tolerant; type and name required):
| Field | Type | Required? | Default |
|---|---|---|---|
type | string | yes | — |
name | string | yes | — |
url | string | null | no | null |
Anchor (tolerant; all optional):
| Field | Type | Default |
|---|---|---|
disambiguation_evidence | string | null | null |
confidence | string | null | null |
PaperStats (tolerant; all integer, default 0): first, last,
middle, unknown, corresponding, total, year_min, year_max.
Minimal valid example (profile.yaml):
name: Jane Doeorcid: 0000-0002-1825-0097field: Computational biologyexpertise: - genomics - data standardstraining: - degree: PhD institution: Example University year: 2015Validate with the recipe above:
check-jsonschema --schemafile schemas/profile_metadata.schema.json jane-doe/profile.yaml.
profile_config (config.json)
Section titled “profile_config (config.json)”Identity plus pipeline bookkeeping. Source model: ProfileConfig (tolerant —
extra keys allowed). Only name is required.
| Field | Type | Required? | Default | Constraints / enum | Description |
|---|---|---|---|---|---|
schema_version | integer | no | 2 | 2 = post-restructure; 1 = legacy | Format marker. |
name | string | yes | — | Researcher display name. | |
level | string enum | no | "full" | lite | full | deep | Profile depth tier. See The level field. |
orcid | string | null | no | null | ORCID pattern (in the JSON schema; model also strips whitespace first) | ORCID identifier. |
affiliation | string | null | no | null | Institutional affiliation. | |
mode | string | no | "synthesize" | Build mode. | |
slug | string | null | no | null | pattern ^[a-z0-9][a-z0-9-]*$ (in the JSON schema) | Directory slug. |
coverage | string | null | no | null | Coverage descriptor. | |
completed_phases | list[string] | no | [] | legacy integer phase numbers are coerced to strings at the model layer | Names of completed pipeline phases. |
phase_retries | object (string -> integer) | no | {} | Per-phase retry counts. | |
grants_source | string enum | null | no | null | grants-data | manual | none | Where grant records come from. A supplied deep input. |
reporter_supplement | boolean | no | false | Opt in to appending NIH RePORTER hits to grants.yaml. | |
cv_source | string | null | no | null | Local path or URL of the person’s CV. A supplied deep input. | |
websites | list[string] | no | [] | The person’s website URLs. Supplied deep inputs. |
Minimal valid example (config.json) — only name is required:
{"name": "Jane Doe"}Fuller example:
{ "name": "Jane Doe", "level": "full", "slug": "jane-doe", "completed_phases": ["metadata", "papers"]}A deep config, which additionally names its supplied inputs:
{ "name": "Jane Doe", "level": "deep", "slug": "jane-doe", "grants_source": "grants-data", "cv_source": "~/Documents/jane-doe-cv.pdf", "websites": ["https://example.edu/~jdoe"]}Validate with the recipe above:
check-jsonschema --schemafile schemas/profile_config.schema.json jane-doe/config.json.
papers_file (sources/papers.yaml)
Section titled “papers_file (sources/papers.yaml)”The wrapper shape is a mapping with a single papers key (a list of
PaperRecord, default []). A bare top-level list is rejected: the model’s
_reject_bare_list validator refuses it as old-format drift, and the JSON schema
independently rejects it because the root type is object, not array.
| Field | Type | Required? | Default | Description |
|---|---|---|---|---|
papers | list[PaperRecord] | no | [] | List of paper records. $refs PaperRecord in $defs. |
PaperRecord (lives under $defs; tolerant — extra keys allowed; only
title required):
| Field | Type | Required? | Default | Constraints / enum |
|---|---|---|---|---|
title | string | yes | — | |
paper_id | string | null | no | null | Citation key; ties papers to summaries and question citations. |
openalex_id | string | null | no | null | |
year | integer | null | no | null | |
first_author | string | null | no | null | |
journal | string | null | no | null | |
full_text_link | string | null | no | null | |
summary | string | null | no | null | Short inline summary (distinct from the summary file). |
type | string | null | no | null | |
access | string | null | no | null | |
citation | string | null | no | null | |
author_position | string | null | no | null | Free string; the pipeline uses first / middle / last / unknown. |
author_index | integer | null | no | null | |
total_authors | integer | null | no | null | |
is_corresponding | boolean | null | no | null | |
identity_verified | boolean | no | false | |
status | string enum | no | "pending" | pending | downloaded | rejected | identity_unverified | skipped_no_input | manual |
download_attempts | integer | null | no | null | |
failed_content_hash | string | null | no | null | |
doi | string | null | no | null | |
url | string | null | no | null | |
pdf_url | string | null | no | null | |
abstract | string | null | no | null | |
venue | string | null | no | null | |
authors | list[string] | null | no | null | |
contaminated | boolean | no | false | Contaminated papers are excluded from synthesis. |
Minimal valid example (sources/papers.yaml):
papers: - title: A representative paper paper_id: smith2020 year: 2020 author_position: firstCounter-example — a bare list fails validation:
# INVALID: top-level list, not {papers: [...]}- title: A representative paper paper_id: smith2020Validate with the recipe above:
check-jsonschema --schemafile schemas/papers_file.schema.json jane-doe/sources/papers.yaml.
grants_file (sources/grants.yaml)
Section titled “grants_file (sources/grants.yaml)”Grant records for a deep profile. lite and full profiles do not have this
file; its absence is not an error.
The wrapper shape mirrors papers_file: a mapping with a grants key (a
list of GrantRecord, default []) plus a schema_version. A bare top-level
list is rejected, by the model’s _reject_bare_list validator and
independently by the schema’s object root type.
| Field | Type | Required? | Default | Description |
|---|---|---|---|---|
schema_version | integer | no | 1 | Format marker. |
grants | list[GrantRecord] | no | [] | List of grant records. $refs GrantRecord in $defs. |
GrantRecord (lives under $defs; tolerant — extra keys allowed; id and
title required):
| Field | Type | Required? | Default | Constraints / enum |
|---|---|---|---|---|
id | string | yes | — | Stable key for the grant; also the chunk source_id in the index. |
title | string | yes | — | |
funder | string | null | no | null | e.g. NIH, NSF. |
number | string | null | no | null | Award / application number. |
role | string enum | null | no | null | pi | co_pi | co_i | other |
status | string enum | null | no | null | funded | pending | completed |
start | string | null | no | null | Start date. |
end | string | null | no | null | End date. |
abstract | string | null | no | null | Indexed alongside the title as grant chunks. |
source | string enum | null | no | null | grants-data | manual | reporter — provenance. |
url | string | null | no | null |
Minimal valid example (sources/grants.yaml):
schema_version: 1grants: - id: nih-r01-example title: Scalable epigenome data infrastructureCounter-example — a bare list fails validation:
# INVALID: top-level list, not {grants: [...]}- id: nih-r01-example title: Scalable epigenome data infrastructureValidate with the recipe above:
check-jsonschema --schemafile schemas/grants_file.schema.json jane-doe/sources/grants.yaml.
cv.md and web/ have no schema
Section titled “cv.md and web/ have no schema”The other two deep sources are Markdown, not structured records, so there is no
JSON Schema for them. Both carry a YAML frontmatter provenance block —
source/url plus fetched_at — which is stripped before indexing. Their
correctness is checked at index time (a source present on disk must produce
chunks) rather than by schema validation.
summary_file (frontmatter of sources/summaries/*.summary.md)
Section titled “summary_file (frontmatter of sources/summaries/*.summary.md)”Source model: SummaryFile (strict — additionalProperties: false). This
schema validates the frontmatter mapping only, never the Markdown body.
| Field | Type | Required? | Default | Description |
|---|---|---|---|---|
paper_id | string | yes | — | Citation key of the summarized paper. |
source_kind | string enum | yes | — | fulltext | abstract. |
source_hash | string | yes | — | SHA-256 of the input text. |
written_at | string | null | no | null | ISO-8601 timestamp. |
Applies to the frontmatter block only
Section titled “Applies to the frontmatter block only”The on-disk file is Markdown with an optional YAML frontmatter block. The schema validates the frontmatter mapping; the Markdown body is never validated. Because the schema forbids extra keys, a consumer must extract the frontmatter and validate that mapping — and must not treat a summary file that has no frontmatter as a schema violation.
Guidance:
- Split on the leading
---fence. - If there is no frontmatter, there is nothing to validate against this schema — skip it, do not reject.
- If frontmatter is present, validate the parsed mapping with the recipe above.
import yamlfrom jsonschema import Draft202012Validatorimport json
def frontmatter(text): if not text.startswith("---"): return None # no frontmatter: nothing to validate parts = text.split("---", 2) if len(parts) < 3: return None return yaml.safe_load(parts[1])
schema = json.load(open("schemas/summary_file.schema.json"))fm = frontmatter(open("jane-doe/sources/summaries/smith2020.summary.md").read())if fm is not None: Draft202012Validator(schema).validate(fm)Minimal valid frontmatter:
paper_id: smith2020source_kind: fulltextsource_hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855written_at: "2026-07-18T00:00:00Z"question (meta/questions.yaml entries)
Section titled “question (meta/questions.yaml entries)”Source model: Question (strict — additionalProperties: false). The schema
validates one entry. meta/questions.yaml is a list, so a consumer
validates each element against this schema.
| Field | Type | Required? | Default | Description |
|---|---|---|---|---|
id | string | yes | — | Question identifier. |
question | string | yes | — | The question text. |
answer | string | null | no | null | Answer, if seeded. |
citations | list[string] | no | [] | Bracketed paper_id references. |
Minimal valid example (a single entry):
id: q1question: What methods does this researcher favor?answer: They emphasize reproducible pipelines.citations: - smith2020Validate each element:
import json, yamlfrom jsonschema import Draft202012Validator
schema = json.load(open("schemas/question.schema.json"))validator = Draft202012Validator(schema)for entry in yaml.safe_load(open("jane-doe/meta/questions.yaml")): validator.validate(entry) # validate each list elementSQLModel mirror
Section titled “SQLModel mirror”The package ships an optional relational representation of a profile in
researcher_profiles.db, for consumers who want to load conforming profiles into
a SQL database instead of reading the on-disk layout. It requires the [sql]
extra:
pip install researcher-profiles[sql]Artifact-to-table mapping:
| JSON-Schema artifact | SQL row / table |
|---|---|
profile_metadata (profile.yaml) | ProfileRow (table profiles) |
papers_file entries (PaperRecord) | PaperRow (table papers) |
profile_metadata.expertise[] strings | ExpertiseTopicRow (table expertise_topics) |
Where the SQL rows intentionally differ from the JSON schema
Section titled “Where the SQL rows intentionally differ from the JSON schema”- Slug as primary key.
ProfileRow.slugis the primary key, butslugis not part ofprofile_metadata— it lives inconfig.json/ProfileConfig. Loaders pass it in explicitly viaProfileRow.from_metadata(slug, meta).PaperRowandExpertiseTopicRowreference it as the foreign keyprofile_slug(ontoprofiles.slug). levelcolumn.ProfileRow.level(indexed, defaultfull) mirrors the metadatalevelfield.- Flattening / subsetting. The SQL rows carry only a curated subset of
fields, not the full tolerant model.
ProfileRowkeeps scalar columns plus a few list/dict fields stored as JSON blobs (subfields,interests,artifacts,anchor) and drops the rest (training,career,not_interests,paper_stats, and so on).PaperRowkeeps a curated subset ofPaperRecord(paper_id,title,year,journal,doi,first_author,author_position,is_corresponding,identity_verified,status) and drops the long tail.
The JSON Schemas — not the SQL tables — are the authoritative on-disk contract. The SQL layer is a lossy convenience projection. See the SQL layer how-to for usage.
Test coverage
Section titled “Test coverage”tests/test_schema_export.py guards the export machinery and the level
contract. It asserts that:
- the core models are covered by the exporter (
profile_metadata,profile_config,papers_file), andprofile_metadatarequiresname; levelis present on bothprofile_metadataandprofile_configwith enum{lite, full, deep}defaulting tofull, and thatlevelround-trips through the models (absent defaults tofull);- export writes files, each a titled JSON-Schema object.
It does not compare the checked-in schemas/*.json bytes to a fresh
export_schemas() run — the regeneration invariant
is a maintainer convention today, not automated drift enforcement.
Note for maintainers: a byte-for-byte drift test (regenerate to a temp dir, compare against the checked-in files) would strengthen the invariant. That is a code change, out of scope for this reference page — raise it separately.
See also
Section titled “See also”- Export JSON Schemas — regeneration how-to.
- Validate a profile — the two validation levels.
- SQL layer — loading profiles into SQL.
- Profile format — what the
leveltiers mean.