Skip to content

JSON Schema reference

The schemas/ directory holds JSON Schema (draft 2020-12) files. Every schema is generated from the package’s Pydantic models in researcher_profiles.schema by schema_export.py. They exist so a consumer can validate profiles without installing this package, in any language with a draft-2020-12 validator.

A profile is one tree, authored and served identically, so each file validates one on-disk artifact. There is no separate set of “published” schemas. profile.jsonld and sources/papers.jsonld are validated by the same schemas whether the profile is on disk or on the web.

FileValidates (on-disk path)Source modelRoot typeadditionalProperties
profile_jsonld.schema.jsonprofile.jsonldProfileDocumentobject + $defsallowed (tolerant)
papers_jsonld.schema.jsonsources/papers.jsonldPapersDocument (wraps PaperRecord via $defs)object + $defsallowed on PaperRecord (tolerant)
grants_jsonld.schema.jsonsources/grants.jsonldGrantsDocument (wraps GrantRecord via $defs)object + $defsallowed on GrantRecord (tolerant)
summary_file.schema.jsonfrontmatter of sources/summaries/*.summary.mdSummaryFileobjectforbidden (strict)

One schema in schemas/ is not an on-disk artifact. It describes a payload handed to another system at export time, and nothing ever writes it into a profile directory. It is published here so a non-Python consumer can validate the payload without installing this package.

FilePayloadModelExtra keys
profile_export_bundle.schema.jsonthe bundle rp export --json / build_export_bundle() hands a knowledge baseProfileExportBundleforbidden (strict)

Its content_hash covers every field except itself and built_at, so a consumer upserts only when the hash changes. See Export for knowledge bases.

These validate the collection-level and embedding artifacts of the researcher-profile specification.

FileValidates (path)Source model
embedding_index.schema.jsonembeddings/index.jsonEmbeddingIndex
profile_list.schema.jsonA hand-authored or dynamically served profile list (see Static API §8); not what rp site writes to a static siteProfileListDocument
collection.schema.jsonCollection bundleProfileCollection
topic_index.schema.jsoncollection/topics.jsonTopicIndexDocument

See the researcher-profile specification for the normative requirements that go beyond what JSON Schema can express (artifact resolution, manifest fidelity, fragment uniqueness, embedding blob integrity, privacy tiers). Those are checked by the two conformance validators over the shared corpus in spec/conformance/.

The schemas emit the JSON-LD names: @context, @id, @type, conformsTo, sameAs, hasPart, subjectOf, not the Python attribute names (conforms_to, id_, same_as). tests/test_schema.py pins that, so a model-config change cannot silently publish Python names into the contract.

“Tolerant” models (profile_jsonld, PaperRecord, GrantRecord) set additionalProperties: true: unknown keys validate and are preserved. Conformance means the baseline fields are present and well-formed; additional keys are permitted, and consumers must ignore unknown keys rather than treat them as failures. The strict model summary_file sets additionalProperties: false.

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
rp 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

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

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.


Both recipes below use only third-party tools, with no researcher-profiles install required.

(a) Python with the third-party jsonschema package

Section titled “(a) Python with the third-party jsonschema package”
import json
from jsonschema import Draft202012Validator
schema = json.load(open("schemas/profile_jsonld.schema.json"))
data = json.load(open("jane-doe/profile.jsonld"))
Draft202012Validator(schema).validate(data) # raises on mismatch

Notes:

  1. Every published artifact is JSON. profile.jsonld, sources/papers.jsonld, and sources/grants.jsonld load with json.load, with no YAML step. Only summary frontmatter is YAML (yaml.safe_load).
  2. $defs resolve internally. The published schemas contain $defs; a compliant draft-2020-12 validator resolves these from the single file, with no extra registry or reference wiring needed.
  3. Pin the dialect. Use Draft202012Validator explicitly rather than validate(...) auto-detection.
  4. Schema validity is not conformance. JSON Schema cannot express “conformsTo must equal this exact IRI” as a hard gate that every validator enforces identically, so check it yourself: a document whose conformsTo is absent or different is not a conforming profile, whatever a validator says.

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

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

check-jsonschema parses both JSON and YAML natively, so the same command works for every artifact. Each per-schema section below refers to “the recipe above” plus its own schema/instance pair.


level is a string enum on profile_jsonld:

  • Type: string
  • Allowed values: "lite", "full", "deep"
  • Default (in the schema): "full"
  • Written explicitly on every profile a build tool emits. A published standard does not leave its depth tier implicit; the schema default exists only so a hand-written minimal document validates.

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 fields live in the build sidecar

Section titled “The deep-input fields live in the build sidecar”

Four fields form the supplied-input contract the deep level is defined by: grants_source, reporter_supplement, cv_source, and websites. They are not in profile_jsonld: they are build inputs, and what a profile publishes is the result (a CV manifest entry, web pages, a grants collection). They live in the build sidecar (.build/<slug>/meta/build_state.json), which has no published schema.

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.


The profile record: a schema:Person node carrying identity, provenance, metadata, and the manifest (hasPart / subjectOf). Source model: ProfileDocument (tolerant, extra keys allowed, string values whitespace-stripped). name, rid, and provenance are required.

This is the one and only profile document: the same profile.jsonld is authored and served, validated by this schema either way. See the profile document.

FieldTypeRequired?DefaultConstraints / enumDescription
@contextstring | nullnothe context IRIThe hosted vocabulary. One line per document; never inlined.
@idstring | nullnoderivedSubject IRI: https://orcid.org/<rid> for an ORCID rid, else url, else #me.
@typestring | nullno"Person"
conformsTostringnothe format IRImust equal https://profiles.databio.org/context/v1.jsonldThe format gate. A document with any other value fails to load.
namestringyesnoneminLength: 1Researcher display name.
ridstringyesnonea canonical ORCID (regex + ISO 7064 checksum) or local:<slug>-<6 hex>The identity and the single cross-system join key. There is no separate orcid key. It is derived from this.
provenancestring enumyesnoneorcid_verified | self_published | third_party | synthetic | historicalWho asserted this profile and on what basis. No default: an unlabeled published assertion is the failure mode this field prevents.
verifiedAtstring | nullnonullrequired when provenance is orcid_verifiedWhen the ORCID round-trip was checked.
licensestring | nullnonullan IRIReuse terms for the published record.
urlstring | nullnonullThe published profile URL. Required by orcid_verified.
dateModifiedstring | nullnonull
sameAslist[string]no[]Other URLs for the same person (Scholar, lab site, homepage).
identifierlist[Identifier]no[]PropertyValue nodesNon-@id identifiers (OpenAlex, Scopus, …).
hasPartlist[ArtifactRef]no[]The manifest: every artifact in the profile, relatively linked.
subjectOflist[ArtifactRef]no[]The persona documents (SOUL, expertise).
levelstring enumno"full"lite | full | deepProfile depth tier. See The level field.
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.
researchOutputslist[ResearchOutput]no[]see ResearchOutput 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.
career_stageCareerStage | nullnonullsee CareerStage sub-tableDate-anchored eligibility facts.

Training (strict; kind, degree and institution required). A postdoc is a kind: postdoc span entry, never a degree. For a degree, year_end is the completion year:

FieldTypeRequired?Default
kinddegree | postdoc | clinical_trainingyesnone
degreestringyesnone
institutionstringyesnone
year_startinteger | nullnonull
year_endinteger | nullnonull
advisorstring | nullnonull

CareerEntry (strict; role and institution required). A null end_year on a held position means “to present”; a null start_year means the start is unknown:

FieldTypeRequired?Default
rolestringyesnone
institutionstringyesnone
start_yearinteger | nullnonull
end_yearinteger | nullnonull

CareerStage (strict). Date-anchored eligibility facts, never verdicts: an evaluator applies a funder’s rule to these at evaluation time. See the spec for the full member table and the NIH R01-equivalent list. as_of, tenure_status, independence, evidence, and confidence are required; every other member may be null/unknown, meaning “not determinable”.

ResearchOutput (tolerant; type and name required). This is the base type for every research output, not only the non-paper ones: PaperRecord subclasses it, and future output kinds (grants, presentations, patents) are meant to subclass it too. It is a JsonLdModel, so every output may carry @id and @type. The type token is free text; the recommended vocabulary is software, dataset, protocol, reagent, model, grant, abstract, presentation, patent, standard:

FieldTypeRequired?Default
typestringyesnone
namestringyesnone
descriptionstring | nullnonull
urlstring | nullnonull
@contextstring | nullnonull
@idstring | nullnonull
@typestring | 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.jsonld):

{
"@context": "https://profiles.databio.org/context/v1.jsonld",
"@id": "https://orcid.org/0000-0002-1825-0097",
"@type": "Person",
"conformsTo": "https://profiles.databio.org/context/v1.jsonld",
"name": "Jane Doe",
"rid": "0000-0002-1825-0097",
"provenance": "third_party",
"field": "Computational biology",
"expertise": ["genomics", "data standards"],
"training": [
{ "kind": "degree", "degree": "PhD", "institution": "Example University",
"year_end": 2015 }
]
}

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


The wrapper is a Collection node whose hasPart holds the works. A bare top-level list is rejected: the model’s _reject_bare_list validator refuses it, and the JSON schema independently rejects it because the root type is object, not array.

FieldTypeRequired?DefaultDescription
@contextstring | nullnothe context IRI
@typestring | nullno"Collection"
conformsTostringnothe format IRIThe format gate.
aboutstring | nullnonullThe @id of the person this collection belongs to.
dateModifiedstring | nullnonullWhen the collection was last reviewed.
hasPartlist[PaperRecord]no[]The works. $refs PaperRecord in $defs.

A work’s @id resolves to the DOI IRI, else the OpenAlex IRI, else #paper/<paper_id>. Bibliographic keys use the schema.org names: name (title), datePublished (xsd:gYear string), isPartOf (a Periodical node), author (a list of Person nodes).

PaperRecord (lives under $defs; tolerant, extra keys allowed; only name required). It subclasses ResearchOutput: a paper is a research output, so it inherits name, type, description and url and adds the bibliographic, authorship and access fields. No build field appears here: identity_verified, status, and contaminated all live in the unpublished build_state.json sidecar instead. The table below has two name columns because several fields are exposed under an ergonomic Python attribute name but stored on disk under the schema.org key (PaperRecord in schema/_sources.py):

On-disk keyPython attributeTypeRequired?DefaultNotes
namename (read as .title)stringyesnoneInherited from ResearchOutput. title= is accepted on construction and .title reads it back.
datePublishedyearinteger | nullnonullxsd:gYear string on disk, coerced to int.
isPartOfjournalstring | nullnonullA Periodical node on disk, flattened to a string.
authorauthorslist[string] | nullnonullA list of Person nodes on disk, flattened to strings.
@contextcontextstring | nullnonull
@idid_string | nullnonullResolves DOI IRI -> OpenAlex IRI -> #paper/<paper_id> when absent.
@typetype_string | nullno"ScholarlyArticle"
paper_idpaper_idstring | nullnonullCitation key; ties papers to summaries.
doidoistring | nullnonull
pmidpmidstring | nullnonull
pmcidpmcidstring | nullnonull
openalex_idopenalex_idstring | nullnonull
venuevenuestring | nullnonull
typetypestring | nullnonullWork type, e.g. authored. Unrelated to @type.
first_authorfirst_authorstring | nullnonull
last_authorlast_authorstring | nullnonull
citationcitationstring | nullnonull
cited_by_countcited_by_countinteger | nullnonull
abstractabstractstring | nullnonull
summarysummarystring | nullnonullShort inline summary (distinct from the summary file).
author_positionauthor_positionstring | nullnonullFree string; the reference build uses first / middle / last / unknown.
author_indexauthor_indexinteger | nullnonull
total_authorstotal_authorsinteger | nullnonull
is_correspondingis_correspondingboolean | nullnonull
open_accessopen_accessboolean | nullnonull
is_oais_oaboolean | nullnonull
oa_statusoa_statusstring | nullnonull
oa_urloa_urlstring | nullnonull
pdf_urlpdf_urlstring | nullnonull
urlurlstring | nullnonull
full_text_linkfull_text_linkstring | nullnonull
accessaccessstring | nullnonull
sourcesourcestring | nullnonull

Minimal valid example (sources/papers.jsonld), a real excerpt from rp-sdk/tests/fixtures/jane-doe/sources/papers.jsonld:

{
"@context": "https://profiles.databio.org/context/v1.jsonld",
"@type": "Collection",
"conformsTo": "https://profiles.databio.org/context/v1.jsonld",
"about": { "@id": "https://orcid.org/0000-0002-1825-0097" },
"hasPart": [
{
"@id": "#paper/doe2016example",
"@type": "ScholarlyArticle",
"name": "ExampleOverlap: enrichment analysis of example region sets",
"paper_id": "doe2016example",
"datePublished": "2016",
"isPartOf": { "@type": "Periodical", "name": "Journal of Synthetic Genomics" },
"author_position": "first"
}
]
}

Counter-example: a bare list fails validation:

// INVALID: top-level list, not {"hasPart": [...]}
[
{ "name": "A representative paper", "paper_id": "smith2020" }
]

Validate with the recipe above: check-jsonschema --schemafile schemas/papers_jsonld.schema.json jane-doe/sources/papers.jsonld.


Grant records for a deep profile. lite and full profiles do not have this file; its absence is not an error.

The wrapper mirrors papers_jsonld: a Collection node whose hasPart holds the grants. 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
@contextstring | nullnothe context IRI
@typestring | nullno"Collection"
conformsTostringnothe format IRIThe format gate.
aboutstring | nullnonullThe @id of the person.
hasPartlist[GrantRecord]no[]The grants. $refs GrantRecord in $defs.

schema.org has no crisp “this person received this grant” relation, so the link from a person to a grant is rp:heldGrant, an rp: term. See context/README.md.

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

FieldTypeRequired?DefaultConstraints / enum
@typestring | nullno"MonetaryGrant"
idstringyesnoneStable key for the grant; also the chunk source_id in the index.
namestringyesnoneThe grant title.
funderstring | nullnonulle.g. NIH, NSF. Serializes as an Organization node; stays a str in Python.
identifierstring | nullnonullAward / application number.
activity_codestring | nullnonullNIH activity code (R01, K99, …), so R01-equivalent history is recoverable without parsing the award 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.jsonld):

{
"@context": "https://profiles.databio.org/context/v1.jsonld",
"@type": "Collection",
"conformsTo": "https://profiles.databio.org/context/v1.jsonld",
"hasPart": [
{
"@type": "MonetaryGrant",
"id": "nih-r01-example",
"name": "Scalable epigenome data infrastructure"
}
]
}

Counter-example: a bare list fails validation:

[ { "id": "nih-r01-example", "name": "Scalable epigenome data infrastructure" } ]

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

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.

FieldTypeRequired?DefaultDescription
paper_idstringyesnoneCitation key of the summarized paper.
source_kindstring enumyesnonefulltext | abstract.
source_hashstringyesnoneSHA-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. A summary file with no frontmatter is not 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"

The package ships an optional SQL backing store in researcher_profiles.db + researcher_profiles.store.sql. It is a peer backend. A profile in the rp_* tables is a profile: it round-trips back to a byte-identical directory, and SqlArtifactStorage reads and writes it through the same ArtifactStorage interface the filesystem backend implements. It requires the sql extra; see the SDK overview for the checkout install.

Artifact-to-table mapping:

JSON-Schema artifactSQL row / table
profile_jsonld (profile.jsonld)ProfileRow.document (table rp_profiles)
papers_jsonld entries (PaperRecord)PaperRow.record (table rp_papers)
grants_jsonld entries (GrantRecord)GrantRow.record (table rp_grants)
profile_jsonld.expertise[] stringsExpertiseTopicRow (table rp_expertise_topics)
profile_jsonld.hasPart / .subjectOf + every file they nameArtifactRow (table rp_artifacts)
build_state.jsonBuildStateRow (table rp_build_state), not published
  • The document is stored whole and is the record. rp_profiles.document holds the entire profile.jsonld payload; rp_papers.record and rp_grants.record hold whole PaperRecord / GrantRecord nodes. Nothing is shredded, because every model here inherits extra="allow" and the format promises unknown terms round-trip untouched. Shredding would drop them.
  • Every scalar column is a derived projection, rebuilt from the document on every write by exactly one writer, never read back into a model. They exist so a store can be queried (for example, “which profiles are level=deep with no R01-equivalent year”), not so it can be reconstructed.
  • rid is the primary key. slug is unique and indexed. That is a store constraint (one store cannot hold two profiles under one handle), not an identity claim. slug is not part of profile_jsonld; it is the directory name, so ingest passes it explicitly. Every child table references profile_rid.
  • Papers are keyed by position. There is no UNIQUE(profile_rid, paper_id): paper_id is a generated citekey and collides within a real profile.
  • date_modified is nullable and never defaulted. Most published profiles legitimately carry no vintage, so the column stays empty rather than getting an invented date.
  • There are no build columns. rp_papers has no status and no identity_verified; those live in rp_build_state, which is outside the published set and can be dropped.
  • Only portable JSON is used: sqlmodel.JSON, never Postgres JSONB, so the same tables load on SQLite.

The JSON Schemas remain the authoritative on-disk contract; the tables are the same content in a second, lossless representation. See How to store profiles in a database for usage.


tests/test_schema.py guards the export machinery and the level contract. It asserts that:

  • the core models are covered by the exporter (profile_jsonld, papers_jsonld, grants_jsonld), and profile_jsonld requires name, rid, and provenance;
  • the exported schemas speak JSON-LD names (@id, conformsTo, sameAs), not Python attribute names;
  • published documents keep additionalProperties: true;
  • the deep-input fields live in the build sidecar, not in profile_jsonld;
  • level is present on profile_jsonld with enum {lite, full, deep} defaulting to full, and round-trips through the models;
  • 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.