How To: Extend the Ed-Fi API - Student Transcript Example
In this example, we add Student Transcript capability to the Ed-Fi API: a new postSecondaryOrganizations resource, four new descriptors, and an extension of the existing studentAcademicRecords resource (and its classRanking common type) with additional properties.
Before you begin:
- This example uses MetaEd to generate the extension schema. MetaEd is a free tool developed by the Ed-Fi Alliance and is the recommended way to add new fields to the Ed-Fi API. Download and install MetaEd before beginning. MetaEd 4.8 or later is required for Ed-Fi API v8.0.
- This example assumes knowledge of the basic concepts described in How To: Extend the Ed-Fi API - Alternative Education Program Example. If you're new to the Ed-Fi technology stack, or haven't used MetaEd before, run through that example first.
- This example assumes the Ed-Fi API is already running locally per the instructions in Getting Started.
- The
api-schema-toolsCLI must be available. See Database Provisioning for installation instructions. - Step 8 verifies the extension with an authenticated API request, which requires API client credentials. Step 5a deletes the database volumes, so any client you created while following Getting Started is removed along with them. You will create a fresh client in Step 8a, after the stack restarts.
Step 1. Design Your Extension
In a real project, you would design your extension as a preliminary step, and analyze how your needs map to the Ed-Fi API data model. We'll propose a design.
This example adds information about where students enroll in college after graduation, an element indicating whether graduates were part of a special education program, and information about whether a transmitted record is an official or unofficial submission.
Based on those needs, we require a new entity (to hold information about
postsecondary institutions) and we need to add elements to two existing
entities. Unlike the Alternative Education Program example, where every new
resource was wholly new, this example also extends existing resources: the
new properties on StudentAcademicRecord and ClassRanking are additions to
resources that already exist in the core Ed-Fi Data Standard.
The InstitutionLevel, InstitutionControl, SubmissionCertification, and
SpecialEducationGraduationStatus elements are modeled as descriptors, the
same enumeration-like pattern used in the Alternative Education Program
example.
Step 2. Author Your Extension Using MetaEd
In this step, we'll create a new project in MetaEd and author our new and extended entities. You need to download and install MetaEd to do this step. Do that now if you haven't already.
Step 2a. Set or Confirm MetaEd Target Version
MetaEd supports multiple Ed-Fi technology stack and data model versions. Confirm that your MetaEd IDE is targeting the desired data model, e.g. ed-fi-model-5.2, by following the instructions in Version Targeting.
MetaEd 4.8 or later is required for Ed-Fi API v8.0 support.
Step 2b. Create a New Extension Project
Create a new extension by following the steps in MetaEd IDE - Creating and
Maintaining Your
Extension.
For this example, place your extension in a folder called StudentTranscript.
Listing of files
ed-fi-model-5.2/
├─ Association/
├─ Choice/
├─ Common/
├─ Descriptor/
├─ Domain/
├─ DomainEntity/
├─ Enumeration/
├─ Interchange/
├─ Shared/
├─ package.json
├─ README.md
StudentTranscript/
├─ Common/
├─ Descriptor/
├─ DomainEntity/
├─ package.json
Step 2c. Update the package.json File
Open the package.json file in your extension project and provide an
appropriate name:
{
"metaEdProject": {
"projectName": "SampleStudentTranscript",
"projectVersion": "1.0.0"
}
}
Click File > Save (Ctrl+S) to save.
Step 2d. Add the PostSecondaryOrganization Domain Entity
Right-click on the folder DomainEntity, and select New File. Name
the new file PostSecondaryOrganization.metaed.
Type or copy and paste the code listing below into your MetaEd file. Note that errors will be listed in the linter panel until the referenced Descriptors are created in a later step.
MetaEd source: PostSecondaryOrganization
Domain Entity PostSecondaryOrganization
documentation "PostSecondaryOrganization"
shared string EdFi.NameOfInstitution
documentation "The name of the institution."
is part of identity
descriptor InstitutionLevel
documentation "The level of the institution."
is required
descriptor InstitutionControl
documentation "The type of control of the institution (i.e., public or private)."
is required
bool AcceptanceIndicator
documentation "An indication of acceptance."
is required
Step 2e. Extend the Student Academic Record Entity
Next, extend the existing StudentAcademicRecord resource to reference the
new entity. Use the additions keyword to add properties to a resource that
already exists in the core Data Standard, rather than declaring a new one.
Right-click on the folder DomainEntity, select New File. Name the
file StudentAcademicRecordExtension.metaed.
MetaEd source: StudentAcademicRecord additions
Domain Entity EdFi.StudentAcademicRecord additions
domain entity PostSecondaryOrganization
documentation "A reference to the postsecondary organization."
is optional
descriptor SubmissionCertification
documentation "The type of submission certification."
is optional
common extension EdFi.ClassRanking
documentation "Class Ranking Extension"
is optional
Step 2f. Extend the Class Ranking Common Type
Similarly, extend the existing ClassRanking common type to add the special
education graduation status element.
Right-click on the Common folder, select New File. Name the file
ClassRankingExtension.metaed.
MetaEd source: ClassRanking additions
Common EdFi.ClassRanking additions
descriptor SpecialEducationGraduationStatus
documentation "The graduation status for special education."
is required
Step 2g. Add the Descriptor Entities
If you're new to Ed-Fi technology, it's worth understanding the Ed-Fi Descriptor pattern because it occurs throughout the model. In essence, Descriptors provide states, districts, vendors, and other platform hosts with the flexibility to use their own enumerations and code sets. A Descriptor is consistent within an operational context such as a single district, but may be different in another operational context.
Right-click on the Descriptor folder, select New File, and add the following four files.
MetaEd source: InstitutionControl Descriptor
Descriptor InstitutionControl
documentation "The type of control for an institution (e.g., public or private)."
MetaEd source: InstitutionLevel Descriptor
Descriptor InstitutionLevel
documentation "The typical level of postsecondary degree offered by the institute."
MetaEd source: SpecialEducationGraduationStatus Descriptor
Descriptor SpecialEducationGraduationStatus
documentation "The graduation status for special education."
MetaEd source: SubmissionCertification Descriptor
Descriptor SubmissionCertification
documentation "The type of submission certification."
Click File > Save All (Ctrl+K S) to save your changes.
Listing of files
StudentTranscript/
├─ Common/
│ └─ ClassRankingExtension.metaed
├─ Descriptor/
│ ├─ InstitutionControl.metaed
│ ├─ InstitutionLevel.metaed
│ ├─ SpecialEducationGraduationStatus.metaed
│ └─ SubmissionCertification.metaed
├─ DomainEntity/
│ ├─ PostSecondaryOrganization.metaed
│ └─ StudentAcademicRecordExtension.metaed
└─ package.json
Step 3. Generate Extended Technical Artifacts Using MetaEd
In this step, we'll build our new MetaEd project. This is fairly straightforward.
Step 3a. Build Your Project
Click Build in the VS Code editor to generate artifacts. Note that you must have a file open for the Build button to be displayed.
Step 3b. View MetaEd Output
You can expand the project in the tree view and click MetaEdOutput to explore generated artifacts. The artifacts include the API schema and XSD files used by the Ed-Fi API, along with SQL scripts and interchange schemas used by the legacy ODS / API.
Listing of files after build
StudentTranscript/
├─ Common/
│ └─ ClassRankingExtension.metaed
├─ Descriptor/
│ ├─ InstitutionControl.metaed
│ ├─ InstitutionLevel.metaed
│ ├─ SpecialEducationGraduationStatus.metaed
│ └─ SubmissionCertification.metaed
├─ DomainEntity/
│ ├─ PostSecondaryOrganization.metaed
│ └─ StudentAcademicRecordExtension.metaed
├─ MetaEdOutput/ <── generated
│ ├─ EdFi/
│ └─ SampleStudentTranscript/
│ ├─ ApiMetadata/
│ ├─ ApiSchema/
│ │ └─ ApiSchema-EXTENSION.json <── used by the Ed-Fi API
│ ├─ Database/
│ ├─ Interchange/
│ └─ XSD/
│ └─ EXTENSION-Ed-Fi-Extended-Core.xsd <── used by the XML Bulk Load Client
└─ package.json
We'll look at how to use the MetaEd output for the Ed-Fi API below.
For Ed-Fi API v8.0, only ApiSchema-EXTENSION.json and (optionally) the XSD
file are needed. The SQL scripts under Database/ and the interchange schemas
are used by the legacy ODS / API and are not required for the new Ed-Fi API.
The XSD file is required only if you load data via the XML Bulk Load Client
Utility.
Step 4. Gather the Schema Files
Create a directory (e.g. my-schemas/) to hold the core and extension
schemas. The prepare-dms-schema.ps1 staging script discovers every file
matching ApiSchema*.json recursively in this directory, so subdirectory
organization is up to you.
my-schemas/
├─ ApiSchema.json <── core schema
└─ ApiSchema-EXTENSION.json <── extension (Step 3b output, copied as-is)
Step 4a. Copy the Core Schema
After running bootstrap-local-dms.ps1 at least once, the core schema is staged
in your repository checkout. Run from the eng/docker-compose/ directory:
New-Item -ItemType Directory -Force "C:\path\to\my-schemas" | Out-Null
Copy-Item ".bootstrap\ApiSchema\schemas\Ed-Fi\ApiSchema.json" "C:\path\to\my-schemas\"
Alternatively, download the EdFi.DataStandard52.ApiSchema package; see
Package
Releases for
the package link and version. A .nupkg is a ZIP archive, and the core schema
is inside it at contentFiles/any/any/ApiSchema/ApiSchema.json:
$package = Get-Item "edfi.datastandard52.apischema.*.nupkg"
Expand-Archive $package.FullName -DestinationPath "./apischema-package" -Force
Copy-Item "./apischema-package/contentFiles/any/any/ApiSchema/ApiSchema.json" "C:\path\to\my-schemas\"
Step 4b. Copy the Extension Schema
Copy ApiSchema-EXTENSION.json from
MetaEdOutput/SampleStudentTranscript/ApiSchema/ (your Step 3b output) into
the same directory. The filename already follows the ApiSchema-*.json
pattern that prepare-dms-schema.ps1 discovers, so no rename is necessary:
Copy-Item "C:\path\to\StudentTranscript\MetaEdOutput\SampleStudentTranscript\ApiSchema\ApiSchema-EXTENSION.json" "C:\path\to\my-schemas\"
Step 5. Stage the Extension Schema
Staging copies your schema and claims into a .bootstrap/ workspace that
bootstrap-local-dms.ps1 reads when it starts the stack and provisions the
database. Run every command below from the eng/docker-compose/ directory, and
run the sub-steps in order: staging writes into the same workspace the
reset in Step 5a clears, so resetting after staging would discard your
extension.
Step 5a. Reset the Local Stack
The Ed-Fi API loads the schema once at startup into a fresh database, so
adding an extension means provisioning a new database rather than mutating the
running one. Stop the stack, delete its database volumes, and clear any existing
.bootstrap/ workspace:
./bootstrap-local-dms.ps1 -d -v
This reset is required, not optional. Following Getting
Started (a prerequisite for this guide) leaves a
core-only .bootstrap/ workspace staged; staging your extension on top of it
makes prepare-dms-schema.ps1 fail with an error like "Existing staged
bootstrap workspace differs from requested inputs... effective schema hash
mismatch." The -d -v teardown removes that workspace and the populated
database volumes so you stage from a clean state.
Step 5b. Build the Schema Tool
prepare-dms-schema.ps1 uses api-schema-tools to hash the combined schema.
Build it once, the same way the Getting
Started flow does:
dotnet build ..\..\src\dms\clis\EdFi.DataManagementService.SchemaTools
prepare-dms-schema.ps1 auto-discovers the build output under the project's
bin/ directory, so no -SchemaToolPath is needed. That output lives outside
.bootstrap/, so it survives the Step 5a reset; you only need to build it once.
Step 5c. Stage the Schema
./prepare-dms-schema.ps1 -ApiSchemaPath "C:\path\to\my-schemas"
prepare-dms-schema.ps1 discovers both ApiSchema*.json files in your
directory, identifies the core (via isExtensionProject: false) and your
extension, computes the combined schema hash, and writes the staged workspace
to .bootstrap/ApiSchema/.
No appsettings.json edit is needed for this local flow. The script copies
your files from -ApiSchemaPath into .bootstrap/ApiSchema/ inside the repo
checkout; after this step, your original directory isn't referenced again.
Writing the staged workspace also creates .bootstrap/bootstrap-manifest.json,
which puts the stack into "bootstrap mode": the next start automatically adds a
compose override that mounts .bootstrap/ApiSchema read-only into the DMS
container at /app/ApiSchema, matching the AppSettings:ApiSchemaPath and
AppSettings:UseApiSchemaPath values already defaulted in .env. The stack you
stopped in Step 5a picks up the staged schema when you start it again in Step 6.
Step 5d. Author and Stage the Claims Fragment
New extension resources are not accessible until they're added to the claims hierarchy and granted to a claim set. This example demonstrates two different situations:
- The four descriptors and
postSecondaryOrganizationsare new resource claims and need to be added to the hierarchy. studentAcademicRecordsis not a new resource claim. Its extra properties ride along under the resource's existing_extnode; the existingstudentAcademicRecordclaim (already granted to claim sets likeSISVendor) already covers them. No claims work is needed for it at all.
Create a directory (e.g. my-claims/) containing a file named
001-sample-student-transcript-claimset.json:
{
"name": "SampleStudentTranscriptClaims",
"resourceClaims": [
{
"isParent": true,
"name": "domains/systemDescriptors",
"children": [
{
"name": "http://ed-fi.org/identity/claims/sample-student-transcript/InstitutionControlDescriptor"
},
{
"name": "http://ed-fi.org/identity/claims/sample-student-transcript/InstitutionLevelDescriptor"
},
{
"name": "http://ed-fi.org/identity/claims/sample-student-transcript/SpecialEducationGraduationStatusDescriptor"
},
{
"name": "http://ed-fi.org/identity/claims/sample-student-transcript/SubmissionCertificationDescriptor"
}
]
},
{
"isParent": true,
"name": "http://ed-fi.org/identity/claims/domains/sample-student-transcript",
"_defaultAuthorizationStrategiesForCrud": [
{
"actionName": "Create",
"authorizationStrategies": [{ "name": "NoFurtherAuthorizationRequired" }]
},
{
"actionName": "Read",
"authorizationStrategies": [{ "name": "NoFurtherAuthorizationRequired" }]
},
{
"actionName": "Update",
"authorizationStrategies": [{ "name": "NoFurtherAuthorizationRequired" }]
},
{
"actionName": "Delete",
"authorizationStrategies": [{ "name": "NoFurtherAuthorizationRequired" }]
},
{
"actionName": "ReadChanges",
"authorizationStrategies": [{ "name": "NoFurtherAuthorizationRequired" }]
}
],
"children": [
{
"name": "http://ed-fi.org/identity/claims/sample-student-transcript/PostSecondaryOrganization"
}
],
"claimSets": [
{
"name": "SISVendor",
"actions": [
{ "name": "Create" },
{ "name": "Read" },
{ "name": "Update" },
{ "name": "Delete" },
{ "name": "ReadChanges" }
]
},
{
"name": "EdFiSandbox",
"actions": [
{ "name": "Create" },
{ "name": "Read" },
{ "name": "Update" },
{ "name": "Delete" },
{ "name": "ReadChanges" }
]
}
]
}
]
}
postSecondaryOrganizations has no relationship to education organizations,
students, or staff, so unlike the descriptors above, there's no existing
domain claim for it to inherit access from. It needs its own domain claim
(domains/sample-student-transcript) with an explicit
_defaultAuthorizationStrategiesForCrud of NoFurtherAuthorizationRequired,
plus explicit claimSets grants, since there's nothing to inherit from.
Compare this to the descriptors and to the Alternative Education Program
example, where every new claim attached under an existing domain and inherited
that domain's grants automatically.
Now stage the claims configuration, including your new fragment:
./prepare-dms-claims.ps1 -ClaimsDirectoryPath "C:\path\to\my-claims"
Because SampleStudentTranscript isn't a recognized built-in extension,
-ClaimsDirectoryPath is required; omitting it fails staging with
ClaimsDirectoryPath is required for unmapped extension project(s): SampleStudentTranscript. prepare-dms-claims.ps1 validates your fragment,
then stages it alongside the built-in claims into .bootstrap/claims/ and
records the staging mode (Hybrid, since a fragment is present) in the
bootstrap manifest.
Step 6. Start the Stack
Start the stack. Because you already stopped it and cleared its database volumes
in Step 5a, no teardown is needed here: starting from the staged workspace
provisions the extension into a clean database. bootstrap-local-dms.ps1 reads
the staged workspace and provisions the database automatically with both the
core and extension schemas, and seeds the Configuration Service with the
built-in claims plus your fragment:
./bootstrap-local-dms.ps1
Both the schema and the claims are loaded once at startup, and only when
their respective databases are empty; there is no hot-reload for either. To
apply a later change to the extension schema or the claims fragment, repeat the
cycle in order: tear down with ./bootstrap-local-dms.ps1 -d -v (which wipes
the databases and clears the staged workspace), re-run
prepare-dms-schema.ps1 / prepare-dms-claims.ps1 to stage the updated files,
then start again with ./bootstrap-local-dms.ps1.
Step 7. Configure Security
The Ed-Fi API is secure by default, but this example needed two different approaches to make everything accessible, both handled when you authored the claims fragment in Step 5:
- The four descriptors inherit access automatically, the same way the
Alternative Education Program example's descriptor did: attaching them as
children of the existing
systemDescriptorsdomain means they inherit whatever claim sets are already granted there (SISVendoralready hasRead, for example). postSecondaryOrganizationsneeded a new domain claim with its ownNoFurtherAuthorizationRequireddefault and explicitclaimSetsgrants, because it has no natural parent domain to inherit from.studentAcademicRecordsneeded no claims change at all. Its extension properties are exposed under the resource's existing_extnode, and authorization is enforced at the resource level, not per-property. Any claim set already granted access tostudentAcademicRecords(for example,SISVendor, which already has full CRUD on therelationshipBasedDatadomain it belongs to) can read and write the extended fields as soon as the stack restarts with the staged schema.
See How To: Create and Manage API Security Metadata for the full explanation of the claims hierarchy and loading modes. Step 8 verifies that access actually works, by making authenticated requests to the new and extended resources.
Step 8. Verify the Extension
Confirm the Ed-Fi API is running with the extended schema by calling the Discovery endpoint:
Invoke-RestMethod http://localhost:8080/api
The response should list your extension's data model under the dataModels
array.
Step 8a. Create API Client Credentials
The -d -v teardown in Step 5a deleted the database volumes, including the
Configuration Service database that holds vendors and applications. Any client
you created while following Getting Started is gone, so create a new one against
the restarted stack. From the eng/docker-compose/ directory:
Import-Module ../smoke_test/modules/SmokeTest.psm1 -Force
$cred = Get-SmokeTestCredential -ConfigServiceUrl "http://localhost:8081"
Write-Host "Key: $($cred.Key)"
Write-Host "Secret: $($cred.Secret)"
See Getting Started - Configure a Data Store for details, including how to associate the client with multiple data stores.
Step 8b. Call the Extension Endpoint
Make an authenticated request to the new resource, using the
<api-client-key>/<api-client-secret> from Step 8a:
$apiToken = Invoke-RestMethod -Method Post -Uri "http://localhost:8081/connect/token" `
-ContentType "application/x-www-form-urlencoded" `
-Body @{
"grant_type" = "client_credentials"
"client_id" = "<api-client-key>"
"client_secret" = "<api-client-secret>"
}
Invoke-RestMethod `
-Uri "http://localhost:8080/api/data/sample-student-transcript/postSecondaryOrganizations" `
-Headers @{ Authorization = "Bearer $($apiToken.access_token)" }
A 200 OK with an empty array confirms that the new resource is active and
the API client's claim set has access.
To confirm the studentAcademicRecords extension specifically, check the
Discovery/OpenAPI document for the resource: its schema now includes an
_ext.samplestudenttranscript node (and, nested under classRanking, a
second _ext.samplestudenttranscript node for
specialEducationGraduationStatusDescriptor). Since the resource itself
already existed, a 200 OK on GET /api/data/ed-fi/studentAcademicRecords alone doesn't confirm the extension is
active; the _ext node in the schema is the reliable check.
Next Steps & Further Information
Congratulations! You have successfully extended the Ed-Fi API with both a new resource and an extended existing resource.
- How To: Create and Manage API Security Metadata: full reference for the claims hierarchy and all loading modes
- Database
Provisioning:
api-schema-toolsCLI reference - The MetaEd Cookbook: examples of common and complex extension scenarios