Generated Application API Reference
Every application that SDCStudio generates from a published data model ships with a Django REST Framework app named api. It is the same in the Lightweight and Enterprise profiles. This page documents that API as the generated code defines it: four POST routes for uploading model files and for validating and storing Governed Data Records.
How to generate and run an application is covered in the App Generation guide.
Base path
The routes are defined in api/urls.py with the URL namespace api:
| Route | View |
|---|---|
upload/zip/ |
ZIPUploadView |
upload/schema/ |
SchemaUploadView |
upload/rdf/ |
RDFUploadView |
validate/xml/ |
XMLValidationView |
The generated project's urls.py mounts the API at /api/v1/, beside the home page, admin/, docs/, accounts/ and each data model app. Every route requires a valid X-API-Key; a request without one is refused with 401 and a WWW-Authenticate: X-API-Key header.
With the default docker-compose.yml, the web service listens on port 18000, so the base URL on the host is http://localhost:18000/api/v1/.
Only POST is defined on each route. There are no list or retrieve endpoints, no pagination and no query filtering.
Authentication
The X-API-Key header
api/authentication.py defines APIKeyAuthentication, and every view sets it as its only authentication class. The class reads the X-API-Key request header, hashes the value with SHA-256 and looks the hash up in the APIKey table.
- A header whose hash matches an active key authenticates the request. The key's
last_used_atandrequest_countare updated on every call. - A header whose hash matches no key is rejected with
401and the body{"detail": "Invalid API key"}. - A header whose key has been deactivated is rejected with
401and the body{"detail": "API key has been deactivated"}. - The
401response carriesWWW-Authenticate: X-API-Key.
When the header is absent, the authentication class returns no user, and the request is refused with 401: the generated settings.py sets REST_FRAMEWORK with APIKeyAuthentication as the default authentication and IsAuthenticated as the default permission, and every view sets IsAuthenticated too.
Creating a key
Keys are created in the generated application's Django admin.
- Open
/admin/and sign in. - Under API, open API Keys and click Add API Key.
- Enter Organization name and Contact email, then click Save.
- Copy the key from the success message. It is shown once. Only its SHA-256 hash and its first twelve characters are stored.
A key has the form sdc4_ followed by a URL-safe random string. Existing keys show only the stored prefix. To revoke a key, open it and clear Is active; the row stays for the audit trail.
Response envelope
Every view returns one of two JSON shapes, defined in api/serializers.py as FileUploadResponseSerializer and ErrorResponseSerializer.
Success:
{
"success": true,
"data": { }
}
Error:
{
"success": false,
"error": {
"code": "FILE_REQUIRED",
"message": "No file provided in request",
"details": {}
}
}
The code, message and details values for each route are listed below, exactly as the views build them.
Storage locations
Uploaded files are written through Django's default storage, whose root is MEDIA_ROOT, set to mediafiles/ in the generated project. Schema and RDF files go to mediafiles/dmlib/, which is also where validation looks for a model's XSD. ZIP files go to mediafiles/zip/.
POST upload/zip/
Stores a ZIP archive, such as another generated application, in mediafiles/zip/. The view only stores the file; it does not install the application. Installation is a separate step, done with the install_app management command or the Install SDC4 App page in the admin.
Request: multipart/form-data with one part named file.
curl -X POST \
-H "X-API-Key: your-api-key" \
-F "file=@app.zip" \
http://localhost:18000/api/v1/upload/zip/
The view checks that the file name ends in .zip and that Python's zipfile can open it and pass an integrity test.
Response 201 Created, with data as ZIPUploadDataSerializer defines it:
{
"success": true,
"data": {
"filename": "app.zip",
"path": "zip/app.zip",
"size": 1048576
}
}
Errors, all 400 Bad Request:
code |
message |
details |
|---|---|---|
FILE_REQUIRED |
No file provided in request | {} |
INVALID_FILE_TYPE |
File must be a ZIP archive | {"filename": ...} |
INVALID_ZIP |
File is not a valid ZIP archive | {"filename": ...} |
POST upload/schema/
Stores one or more model files in mediafiles/dmlib/.
Request: multipart/form-data with one or more parts named files.
curl -X POST \
-H "X-API-Key: your-api-key" \
-F "files=@dm-abc123.xsd" \
-F "files=@dm-abc123.html" \
http://localhost:18000/api/v1/upload/schema/
Each file is checked separately:
- the extension must be
.xsd,.htmlor.owl; - the name must start with
dm-and contain exactly one dot, that is the patterndm-{ct_id}.{ext}.
A file that fails either check is reported in errors and skipped; the others are stored. The response is 201 Created whether or not any file was stored, with data as SchemaUploadDataSerializer defines it:
{
"success": true,
"data": {
"stored_files": [
{"filename": "dm-abc123.xsd", "path": "dmlib/dm-abc123.xsd", "size": 24576}
],
"errors": [
{"filename": "schema.xsd", "error": "Filename must match pattern: dm-{ct_id}.{ext}"}
]
}
}
The per-file error strings are Invalid file extension. Allowed: .xsd, .html, .owl and Filename must match pattern: dm-{ct_id}.{ext}.
Error, 400 Bad Request:
code |
message |
details |
|---|---|---|
FILES_REQUIRED |
No files provided in request | {} |
POST upload/rdf/
Stores a Turtle or RDF/XML file in mediafiles/dmlib/ and, in the Enterprise profile, also loads it into GraphDB.
Request: multipart/form-data with one part named file. The extension must be .ttl or .rdf.
curl -X POST \
-H "X-API-Key: your-api-key" \
-F "file=@dm-abc123.ttl" \
http://localhost:18000/api/v1/upload/rdf/
After the file is stored, the view checks the settings GRAPHDB_URL and GRAPHDB_REPOSITORY. When both are set, it derives the model identifier from the file name by removing dm-, the extension and any _shacl suffix, builds the named graph urn:sdc4:dm-{ct_id}:schema, and posts the file to {GRAPHDB_URL}/repositories/{GRAPHDB_REPOSITORY}/statements with that graph as the context. A 200, 201 or 204 from GraphDB gives triplestore_status synced; anything else, including a connection error, gives failed. The file stays stored either way.
When the settings are not both present, triplestore_status is disabled and graph_uri is null. The Lightweight profile defines FUSEKI_URL rather than GRAPHDB_URL, so in a Lightweight application this route always reports disabled; it does not load the file into Fuseki.
Response 201 Created, with data as RDFUploadDataSerializer defines it:
{
"success": true,
"data": {
"filename": "dm-abc123.ttl",
"path": "dmlib/dm-abc123.ttl",
"size": 15360,
"triplestore_status": "synced",
"graph_uri": "urn:sdc4:dm-abc123:schema"
}
}
triplestore_status is one of synced, failed, disabled.
Errors, all 400 Bad Request:
code |
message |
details |
|---|---|---|
FILE_REQUIRED |
No file provided in request | {} |
INVALID_FILE_TYPE |
File must be .ttl or .rdf | {"filename": ...} |
POST validate/xml/
Validates a Governed Data Record against its model's XSD and stores it.
Request: either multipart/form-data with one part named file, or a JSON or form body with a field xml_content holding the XML as a string.
curl -X POST \
-H "X-API-Key: your-api-key" \
-F "file=@instance.xml" \
http://localhost:18000/api/v1/validate/xml/
What the view does
- Parses the XML with lxml. A parse failure is
INVALID_XML. - Reads the root element's local name, which must start with
dm-. The rest of the name is the model identifier,dm_ct_id. - Looks for
mediafiles/dmlib/dm-{dm_ct_id}.xsd. If it is not there, the response isSCHEMA_NOT_FOUND. The file is placed there when the application is generated and can also be uploaded throughupload/schema/. - Validates the record with
SDCValidator(schema_path).validate_and_recover(xml_content)fromsdc4_shared/utils/sdc_validator.py. The validator builds the schema withsdcvalidatoras XSD 1.1 and resolves the reference modelsdc4.xsdlocally: throughmediafiles/dmlib/catalog.xmlwhen it is present, otherwise from asdc4.xsdbeside the model's XSD, otherwise over the network. - Decides the outcome:
- The record is valid:
validation_statusisvalid. - The record has semantic errors that the validator could recover by inserting exceptional values:
validation_statusisvalid_with_ev,auto_corrected_fieldslists the XPaths that were corrected,validation_errorsmaps each XPath to its reason, and the recovered XML is what gets stored. - The record has structural errors that cannot be recovered, or validation itself raised: the response is
VALIDATION_FAILEDwith status422. - Generates the record identifier with
cuid2:i-{cuid}for a valid record,i-ev-{cuid}for a recovered one. - Checks the DM registry (
sdc4_shared/utils/dm_registry.py) for an installed app whose model declaresDM_CT_IDequal todm_ct_id, and reports the answer asdedicated_app. Either way the record is written togeneric_storage.GenericInstancewith its XML, label, validation status, errors and corrected fields, which the admin lists, andstorage_typeisgeneric. - Reads
dm_labelfrom the record'sdm-labelelement, falling back toDM-{dm_ct_id}. - Sets
rdf_sync_statustodisabled. Records stored through this route are not written to the triplestore, in either profile; the data model apps upload RDF for their own records when they are saved.
Exceptional values
The API does not accept or return exceptional values as separate fields. They appear in three places:
- in the stored XML, as the
ExceptionalValueelements that the validator inserts during recovery; - in
validation_status, which isvalid_with_evwhen any were inserted; - in the
i-ev-prefix ofinstance_idand theauto_corrected_fieldslist.
Records stored as GenericInstance can be inspected in the admin under Generic Instance Storage.
Response
201 Created, with data as the view builds it (XMLValidationDataSerializer describes the same fields, except dedicated_app):
{
"success": true,
"data": {
"instance_id": "i-ev-tz4a98xxat96iws9zmbrgj3a",
"dm_ct_id": "abc123",
"dm_label": "Blood Pressure",
"validation_status": "valid_with_ev",
"storage_type": "generic",
"dedicated_app": false,
"auto_corrected_fields": ["/sdc4:dm-abc123/sdc4:data/sdc4:ms-xyz/sdc4:xdstring-value"],
"validation_errors": {"/sdc4:dm-abc123/sdc4:data/sdc4:ms-xyz/sdc4:xdstring-value": "..."},
"rdf_sync_status": "disabled"
}
}
Field values:
| Field | Values |
|---|---|
validation_status |
valid, valid_with_ev |
storage_type |
generic (always) |
dedicated_app |
true when a data model app for this dm_ct_id is installed, else false |
auto_corrected_fields |
list of XPath strings; empty when valid |
validation_errors |
object mapping XPath to reason, or null when valid |
rdf_sync_status |
disabled (always) |
Errors
| Status | code |
message |
details |
|---|---|---|---|
400 |
XML_REQUIRED |
No XML content provided | {} |
400 |
INVALID_XML |
Could not parse XML | {"error": ...} |
400 |
INVALID_XML |
Root element must be <sdc4:dm-{ct_id}> |
{"root_tag": ...} |
404 |
SCHEMA_NOT_FOUND |
No schema found for dm-{ct_id} | {"dm_ct_id": ...} |
422 |
VALIDATION_FAILED |
XML failed structural validation | {"errors": {xpath: reason}} |
422 |
VALIDATION_FAILED |
XML validation failed | {"error": ...} |
Status codes used
| Status | Where |
|---|---|
201 Created |
every successful call |
400 Bad Request |
missing or malformed input, as listed per route |
401 Unauthorized |
no X-API-Key, or one that is unknown or deactivated |
404 Not Found |
validate/xml/ when the model's XSD is not in mediafiles/dmlib/ |
422 Unprocessable Entity |
validate/xml/ when the record fails structural validation or validation raises |
Lightweight and Enterprise
The api app, its routes, serializers and authentication are identical in both profiles. The only differences come from settings:
- Enterprise defines
GRAPHDB_URLandGRAPHDB_REPOSITORY, soupload/rdf/loads files into GraphDB. - Lightweight defines
FUSEKI_URLinstead.upload/rdf/does not talk to Fuseki and reportsdisabled. validate/xml/reportsrdf_sync_statusdisabledin both.
Tests
The app ships with api/tests.py. Run it inside the web container:
docker compose exec web python manage.py test api
Questions about the generated API: contact@axius-sdc.com