Skip to content

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.

FileValidates (on-disk path)Source modelRoot typeadditionalProperties
profile_metadata.schema.jsonprofile.yamlProfileMetadataobject + $defsallowed (tolerant)
profile_config.schema.jsonconfig.jsonProfileConfigobjectallowed (tolerant)
papers_file.schema.jsonsources/papers.yamlPapersFile (wraps PaperRecord via $defs)object + $defsallowed on PaperRecord (tolerant)
grants_file.schema.jsonsources/grants.yaml (deep profiles)GrantsFile (wraps GrantRecord via $defs)object + $defsallowed on GrantRecord (tolerant)
summary_file.schema.jsonfrontmatter of sources/summaries/*.summary.mdSummaryFileobjectforbidden (strict)
question.schema.jsonentries of meta/questions.yamlQuestionobjectforbidden (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.


The checked-in schemas/*.json files are generated artifacts. Regenerate them from the models with the CLI:

Terminal window
researcher-profiles schema export schemas/

or programmatically:

from researcher_profiles.schema_export import export_schemas, build_schemas
export_schemas("schemas") # writes <name>.schema.json files
build_schemas() # returns {name: schema_dict} without writing

Invariant: 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.


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, yaml
from jsonschema import Draft202012Validator
schema = json.load(open("schemas/profile_metadata.schema.json"))
data = yaml.safe_load(open("jane-doe/profile.yaml")) # YAML -> JSON-compatible dict
Draft202012Validator(schema).validate(data) # raises on mismatch

Notes:

  1. Parse YAML first. profile.yaml, sources/papers.yaml, meta/questions.yaml, and summary frontmatter are YAML — load them to JSON-compatible data with yaml.safe_load before validating. config.json is already JSON (json.load).
  2. $defs resolve internally. The papers_file and profile_metadata schemas contain $defs; a compliant draft-2020-12 validator resolves these from the single file — no extra registry or reference wiring is needed.
  3. Pin the dialect. Use Draft202012Validator explicitly rather than validate(...) auto-detection.

(b) check-jsonschema CLI (no Python authoring)

Section titled “(b) check-jsonschema CLI (no Python authoring)”
Terminal window
check-jsonschema --schemafile schemas/profile_metadata.schema.json jane-doe/profile.yaml

check-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.


level is a string enum, present on both profile_config and profile_metadata:

  • Type: string
  • Allowed values: "lite", "full", "deep"
  • Default: "full"
  • An absent level validates and is treated as full, 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.

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.


Top-level profile metadata. Source model: ProfileMetadata (tolerant base — extra keys allowed, string values whitespace-stripped). Only name is required.

FieldTypeRequired?DefaultConstraints / enumDescription
namestringyesminLength: 1Researcher display name.
levelstring enumno"full"lite | full | deepProfile depth tier. See The level field.
orcidstring | nullnonullORCID pattern ^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$ (in the JSON schema; the model additionally strips surrounding whitespace before matching)ORCID identifier.
affiliationstring | nullnonullInstitutional affiliation.
scholar_urlstring | nullnonullGoogle Scholar profile URL.
openalex_idstring | nullnonullOpenAlex author ID.
fieldstring | nullnonullPrimary field.
subfieldslist[string]no[]Subfield labels.
summarystring | nullnonullProse overview.
traininglist[Training]no[]see Training sub-tableEducation / training history.
careerlist[CareerEntry]no[]see CareerEntry sub-tablePositions held.
expertiselist[string]no[]Expertise topic labels.
interestslist[string]no[]Research interests.
not_interestslist[string]no[]Explicit non-interests.
methodological_commitmentslist[string]no[]Methodological stances.
recurring_positionslist[string]no[]Positions taken repeatedly.
intellectual_lineagelist[string]no[]Intellectual influences.
critiqueslist[string]no[]Recurring critiques.
artifactslist[Artifact]no[]see Artifact sub-tableSoftware / datasets / other outputs.
collaboratorslist[string | object]no[]each item is a string OR {name, affiliation, relationship} objectCollaborators.
anchorAnchor | nullnonullsee Anchor sub-tableDisambiguation evidence.
paper_statsPaperStats | nullnonullsee PaperStats sub-tableAuthor-position counts and year range.

Training (tolerant; degree and institution required):

FieldTypeRequired?Default
degreestringyes
institutionstringyes
yearinteger | string | nullnonull
advisorstring | nullnonull

CareerEntry (tolerant; all three required). years coerces int to string at the model layer.

FieldTypeRequired?
rolestringyes
institutionstringyes
yearsstringyes

Artifact (tolerant; type and name required):

FieldTypeRequired?Default
typestringyes
namestringyes
urlstring | nullnonull

Anchor (tolerant; all optional):

FieldTypeDefault
disambiguation_evidencestring | nullnull
confidencestring | nullnull

PaperStats (tolerant; all integer, default 0): first, last, middle, unknown, corresponding, total, year_min, year_max.

Minimal valid example (profile.yaml):

name: Jane Doe
orcid: 0000-0002-1825-0097
field: Computational biology
expertise:
- genomics
- data standards
training:
- degree: PhD
institution: Example University
year: 2015

Validate with the recipe above: check-jsonschema --schemafile schemas/profile_metadata.schema.json jane-doe/profile.yaml.


Identity plus pipeline bookkeeping. Source model: ProfileConfig (tolerant — extra keys allowed). Only name is required.

FieldTypeRequired?DefaultConstraints / enumDescription
schema_versionintegerno22 = post-restructure; 1 = legacyFormat marker.
namestringyesResearcher display name.
levelstring enumno"full"lite | full | deepProfile depth tier. See The level field.
orcidstring | nullnonullORCID pattern (in the JSON schema; model also strips whitespace first)ORCID identifier.
affiliationstring | nullnonullInstitutional affiliation.
modestringno"synthesize"Build mode.
slugstring | nullnonullpattern ^[a-z0-9][a-z0-9-]*$ (in the JSON schema)Directory slug.
coveragestring | nullnonullCoverage descriptor.
completed_phaseslist[string]no[]legacy integer phase numbers are coerced to strings at the model layerNames of completed pipeline phases.
phase_retriesobject (string -> integer)no{}Per-phase retry counts.
grants_sourcestring enum | nullnonullgrants-data | manual | noneWhere grant records come from. A supplied deep input.
reporter_supplementbooleannofalseOpt in to appending NIH RePORTER hits to grants.yaml.
cv_sourcestring | nullnonullLocal path or URL of the person’s CV. A supplied deep input.
websiteslist[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.


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.

FieldTypeRequired?DefaultDescription
paperslist[PaperRecord]no[]List of paper records. $refs PaperRecord in $defs.

PaperRecord (lives under $defs; tolerant — extra keys allowed; only title required):

FieldTypeRequired?DefaultConstraints / enum
titlestringyes
paper_idstring | nullnonullCitation key; ties papers to summaries and question citations.
openalex_idstring | nullnonull
yearinteger | nullnonull
first_authorstring | nullnonull
journalstring | nullnonull
full_text_linkstring | nullnonull
summarystring | nullnonullShort inline summary (distinct from the summary file).
typestring | nullnonull
accessstring | nullnonull
citationstring | nullnonull
author_positionstring | nullnonullFree string; the pipeline uses first / middle / last / unknown.
author_indexinteger | nullnonull
total_authorsinteger | nullnonull
is_correspondingboolean | nullnonull
identity_verifiedbooleannofalse
statusstring enumno"pending"pending | downloaded | rejected | identity_unverified | skipped_no_input | manual
download_attemptsinteger | nullnonull
failed_content_hashstring | nullnonull
doistring | nullnonull
urlstring | nullnonull
pdf_urlstring | nullnonull
abstractstring | nullnonull
venuestring | nullnonull
authorslist[string] | nullnonull
contaminatedbooleannofalseContaminated papers are excluded from synthesis.

Minimal valid example (sources/papers.yaml):

papers:
- title: A representative paper
paper_id: smith2020
year: 2020
author_position: first

Counter-example — a bare list fails validation:

# INVALID: top-level list, not {papers: [...]}
- title: A representative paper
paper_id: smith2020

Validate with the recipe above: check-jsonschema --schemafile schemas/papers_file.schema.json jane-doe/sources/papers.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.

FieldTypeRequired?DefaultDescription
schema_versionintegerno1Format marker.
grantslist[GrantRecord]no[]List of grant records. $refs GrantRecord in $defs.

GrantRecord (lives under $defs; tolerant — extra keys allowed; id and title required):

FieldTypeRequired?DefaultConstraints / enum
idstringyesStable key for the grant; also the chunk source_id in the index.
titlestringyes
funderstring | nullnonulle.g. NIH, NSF.
numberstring | nullnonullAward / application number.
rolestring enum | nullnonullpi | co_pi | co_i | other
statusstring enum | nullnonullfunded | pending | completed
startstring | nullnonullStart date.
endstring | nullnonullEnd date.
abstractstring | nullnonullIndexed alongside the title as grant chunks.
sourcestring enum | nullnonullgrants-data | manual | reporter — provenance.
urlstring | nullnonull

Minimal valid example (sources/grants.yaml):

schema_version: 1
grants:
- id: nih-r01-example
title: Scalable epigenome data infrastructure

Counter-example — a bare list fails validation:

# INVALID: top-level list, not {grants: [...]}
- id: nih-r01-example
title: Scalable epigenome data infrastructure

Validate with the recipe above: check-jsonschema --schemafile schemas/grants_file.schema.json jane-doe/sources/grants.yaml.

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 (strictadditionalProperties: false). This schema validates the frontmatter mapping only, never the Markdown body.

FieldTypeRequired?DefaultDescription
paper_idstringyesCitation key of the summarized paper.
source_kindstring enumyesfulltext | abstract.
source_hashstringyesSHA-256 of the input text.
written_atstring | nullnonullISO-8601 timestamp.

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 yaml
from jsonschema import Draft202012Validator
import 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: smith2020
source_kind: fulltext
source_hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
written_at: "2026-07-18T00:00:00Z"

Source model: Question (strictadditionalProperties: false). The schema validates one entry. meta/questions.yaml is a list, so a consumer validates each element against this schema.

FieldTypeRequired?DefaultDescription
idstringyesQuestion identifier.
questionstringyesThe question text.
answerstring | nullnonullAnswer, if seeded.
citationslist[string]no[]Bracketed paper_id references.

Minimal valid example (a single entry):

id: q1
question: What methods does this researcher favor?
answer: They emphasize reproducible pipelines.
citations:
- smith2020

Validate each element:

import json, yaml
from 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 element

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:

Terminal window
pip install researcher-profiles[sql]

Artifact-to-table mapping:

JSON-Schema artifactSQL row / table
profile_metadata (profile.yaml)ProfileRow (table profiles)
papers_file entries (PaperRecord)PaperRow (table papers)
profile_metadata.expertise[] stringsExpertiseTopicRow (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.slug is the primary key, but slug is not part of profile_metadata — it lives in config.json / ProfileConfig. Loaders pass it in explicitly via ProfileRow.from_metadata(slug, meta). PaperRow and ExpertiseTopicRow reference it as the foreign key profile_slug (onto profiles.slug).
  • level column. ProfileRow.level (indexed, default full) mirrors the metadata level field.
  • Flattening / subsetting. The SQL rows carry only a curated subset of fields, not the full tolerant model. ProfileRow keeps 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). PaperRow keeps a curated subset of PaperRecord (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.


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), and profile_metadata requires name;
  • level is present on both profile_metadata and profile_config with enum {lite, full, deep} defaulting to full, and that level round-trips through the models (absent defaults to full);
  • 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.