Skip to main content
Glama
jsonschema.ts7.02 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { InternalSchemaElement, InternalTypeSchema } from '@medplum/core'; import { capitalize, getAllDataTypes, indexStructureDefinitionBundle } from '@medplum/core'; import { readJson } from '@medplum/definitions'; import type { Bundle, ElementDefinitionType, StructureDefinition } from '@medplum/fhirtypes'; import { writeFileSync } from 'fs'; import type { JSONSchema6, JSONSchema6Definition } from 'json-schema'; import { resolve } from 'path'; import { getValueSetValues } from './valuesets'; // Generate fhir.schema.json // // The FHIR spec "Downloads" page includes "whole specification", which includes "fhir.schema.json". // We extend the "fhir.schema.json" file with Medplum-specific extensions. // See: https://hl7.org/fhir/R4/downloads.html // // This tool *could* be used to generate all of "fhir.schema.json", however - // there are a number of inconsistencies in the original version that appear to be the result of // evolution rather than intentional design. // // For example // 1. Sometimes the "id" element is defined as a "string" rather than "id" type. // 2. Sometimes "resourceType" is a required field // 3. Sometimes properties include "pattern" with the regex definition of primitive type. // 4. Sometimes properties embed enum values vs use "code". // // Rather than risk breaking existing tools, we extend the existing schema with Medplum-specific definitions. // This allows us to use the existing schema as a starting point, and only add new definitions. interface FhirSchema extends JSONSchema6 { id: 'http://hl7.org/fhir/json-schema/4.0'; discriminator: { propertyName: 'resourceType'; mapping: Record<string, string>; }; oneOf: JSONSchema6Definition[]; definitions: { ResourceList: { oneOf: JSONSchema6Definition[]; }; [k: string]: JSONSchema6Definition; }; } export function main(): void { indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); const medplumBundle = readJson('fhir/r4/profiles-medplum.json') as Bundle<StructureDefinition>; const medplumTypes = medplumBundle.entry?.map((e) => e.resource?.id) ?? []; indexStructureDefinitionBundle(medplumBundle); // Start with the existing schema const fhirSchema = readJson('fhir/r4/fhir.schema.json') as FhirSchema; // Then add element types for (const typeSchema of Object.values(getAllDataTypes())) { const typeName = typeSchema.name; if (medplumTypes.includes(typeName)) { addSchemaDefinition(fhirSchema, typeSchema); for (const innerType of typeSchema.innerTypes) { addSchemaDefinition(fhirSchema, innerType); } } } writeFileSync( resolve(import.meta.dirname, '../../definitions/dist/fhir/r4/fhir.schema.json'), JSON.stringify(fhirSchema, undefined, 2) .replaceAll("'", '\\u0027') .replaceAll('<', '\\u003c') .replaceAll('=', '\\u003d') .replaceAll('>', '\\u003e'), 'utf8' ); } function addSchemaDefinition(fhirSchema: FhirSchema, typeSchema: InternalTypeSchema): void { const typeName = typeSchema.name; if (!fhirSchema.discriminator.mapping[typeName]) { fhirSchema.discriminator.mapping[typeName] = `#/definitions/${typeName}`; } if (!fhirSchema.oneOf.find((x) => typeof x === 'object' && x.$ref === `#/definitions/${typeName}`)) { fhirSchema.oneOf.push({ $ref: `#/definitions/${typeName}` }); } if ( !fhirSchema.definitions.ResourceList.oneOf.find( (x) => typeof x === 'object' && x.$ref === `#/definitions/${typeName}` ) ) { fhirSchema.definitions.ResourceList.oneOf.push({ $ref: `#/definitions/${typeName}` }); } fhirSchema.definitions[typeName] = buildElementSchema(typeSchema); } function buildElementSchema(typeSchema: InternalTypeSchema): JSONSchema6Definition { const { properties, required } = buildProperties(typeSchema); return { description: typeSchema.description, properties, additionalProperties: false, required, }; } function buildProperties(typeSchema: InternalTypeSchema): { properties: Record<string, JSONSchema6Definition>; required: string[] | undefined; } { const properties: Record<string, JSONSchema6Definition> = {}; let required: string[] | undefined = undefined; if (typeSchema.kind === 'resource') { properties['resourceType'] = { description: `This is a ${typeSchema.name} resource`, const: typeSchema.name, }; required = ['resourceType']; } for (const [path, elementDefinition] of Object.entries(typeSchema.elements)) { for (const elementDefinitionType of elementDefinition?.type ?? []) { const propertyName = path.replace('[x]', capitalize(elementDefinitionType.code as string)); properties[propertyName] = buildPropertySchema(elementDefinition, elementDefinitionType, path); } if (!path.includes('[x]') && elementDefinition?.min) { if (!required) { required = []; } required.push(path); } } return { properties, required }; } function buildPropertySchema( elementDefinition: InternalSchemaElement, elementDefinitionType: ElementDefinitionType, path: string ): JSONSchema6Definition { const result: JSONSchema6Definition = { description: elementDefinition.description, }; const enumValues = getEnumValues(elementDefinition); if (elementDefinition.max > 1) { result.items = {}; if (enumValues) { result.items.enum = enumValues; } else { result.items.$ref = `#/definitions/${getTypeName(path, elementDefinitionType)}`; } result.type = 'array'; } else if (enumValues) { result.enum = enumValues; } else { result.$ref = `#/definitions/${getTypeName(path, elementDefinitionType)}`; } return result; } function getTypeName(path: string, elementDefinitionType: ElementDefinitionType): string { if (path.endsWith('.id')) { return 'id'; } const code = elementDefinitionType.code as string; return code === 'BackboneElement' || code === 'Element' ? buildTypeName(path.split('.') as string[]) : code; } function buildTypeName(components: string[]): string { if (components.length === 1) { return components[0]; } return components.map(capitalize).join('_'); } const excludedValueSets = [ 'http://hl7.org/fhir/ValueSet/resource-types|4.0.1', 'http://hl7.org/fhir/ValueSet/all-types|4.0.1', 'http://hl7.org/fhir/ValueSet/defined-types|4.0.1', ]; function getEnumValues(elementDefinition: InternalSchemaElement): string[] | undefined { const valueSet = elementDefinition.binding?.valueSet; if (valueSet) { if (!excludedValueSets.includes(valueSet)) { const values = getValueSetValues(valueSet); if (values && values.length > 0) { return values; } } } return undefined; } if (import.meta.main) { main(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server