Metadata Translation Layers for API Endpoints #8288
grantfitzsimmons
started this conversation in
Feature Requests
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Problem Statement
Today, when an API consumer fetches a resource from
/api/specify/<model>/<id>/, the response JSON uses internal Django model field names as keys:{ "id": 1001, "catalogNumber": "CAS-12345", "fieldNumber": "GRF-2023-001", "collectionObjectAttribute": "/api/specify/collectionobjectattribute/500/", "resource_uri": "/api/specify/collectionobject/1001/" }To display human-readable field labels (e.g., "Catalog Number" in English, "Numéro de catalogue" in French, user-assigned overrides), the consumer must:
/context/schema_localization.json?lang=frto get the localized schema metadatatables → collectionobject → items → catalogNumber → namepathThis two-step workflow is error-prone, adds latency, and requires every consumer to reimplement the same mapping logic.
The goal is to allow API consumers to receive localized field labels directly in API data responses, eliminating the need for a separate schema localization fetch. This must support all languages currently available in
Splocaleitemstr(user-assigned captions), with sensible fallback to English and then to the internal field name. Full backward compatibility must be maintained — existing API responses must remain unchanged unless the consumer explicitly opts in. The feature must work for both read (GET single resource, GET collection list) and write (POST, PUT) endpoints, support nested/inlined dependent objects, minimize performance impact, and respect collection-level and discipline-level schema customization.Out of Scope
Opt-In Mechanism
The API consumer SHALL opt in to receiving localized field labels by including a query parameter
fieldLabels=<lang>on any data endpoint request.GET /api/specify/collectionobject/1001/?fieldLabels=fr<lang>value SHALL use the samell[-cc]format as the existingschema_localizationendpoint (e.g.,en,en-us,fr,uk-ua).When
fieldLabelsis absent, the response SHALL be identical to current behavior (no localization metadata included).When
fieldLabelsis present but the requested language has no translations, the system SHALL fall back following this chain:language-country(exact match, e.g.,en-us)languagewith no country (e.g.,en)enwith no country (English fallback)splocalecontaineritem.name(the untranslated name)Response Format for Single Resources
For single-resource responses (
GET /api/specify/<model>/<id>/), the response SHALL include a top-level_fieldLabelsobject whenfieldLabelsis specified:{ "id": 1001, "catalogNumber": "CAS-12345", "fieldNumber": "GRF-2023-001", "collectionObjectAttribute": "/api/specify/collectionobjectattribute/500/", "resource_uri": "/api/specify/collectionobject/1001/", "_fieldLabels": { "id": "ID", "catalogNumber": "Numéro de catalogue", "fieldNumber": "Numéro de terrain", "collectionObjectAttribute": "Attribut de l'objet de collection" } }The
_fieldLabelskey SHALL be a flat object mapping each top-level field name to its localized caption (thenamefromsplocalecontaineritemplus itsSplocaleitemstrtranslation for the requested language). For fields that reference related objects (foreign keys shown as URIs), the label SHALL be the localized table/relationship name from the schema container for the parent table. Theresource_uriand_fieldLabelskeys SHALL NOT themselves have entries in_fieldLabels.Response Format for Collections
For collection responses (
GET /api/specify/<model>/), the response SHALL include_fieldLabelsat the same level asobjects(the metadata object, not inside each individual object):{ "objects": [ { "id": 1001, "catalogNumber": "CAS-12345", ... }, ... ], "meta": { "limit": 20, "offset": 0, "total_count": 156 }, "_fieldLabels": { "id": "ID", "catalogNumber": "Numéro de catalogue", ... } }The
_fieldLabelsSHALL NOT be repeated inside each object in theobjectsarray — it SHALL appear once at the top level of the response.Nested / Inlined Objects
When a dependent (inlined) object is included in the response, its fields SHALL have their own
_fieldLabelsblock nested within the dependent object:{ "id": 1001, "catalogNumber": "CAS-12345", "collectionObjectAttribute": { "id": 500, "text1": "Some note", "integer1": 3, "resource_uri": "/api/specify/collectionobjectattribute/500/", "_fieldLabels": { "id": "ID", "text1": "Remarque", "integer1": "Nombre d'éléments" } }, "resource_uri": "/api/specify/collectionobject/1001/", "_fieldLabels": { "id": "ID", "catalogNumber": "Numéro de catalogue", "collectionObjectAttribute": "Attribut de l'objet de collection" } }The
_fieldLabelsSHALL be included at every nesting level where objects are inlined (i.e., where the full object data is present, not just a URI reference).Write Operations
POST and PUT responses that return the created/updated object SHALL include
_fieldLabelsin the response body when thefieldLabelsquery parameter is present on the request. ThefieldLabelsparameter SHALL be accepted but ignored for the purposes of request parsing — it only affects the response format.Language Discovery
The existing
/context/schema/language/endpoint SHALL continue to serve as the discovery mechanism for available schema localization languages. If a consumer requests a language that exists in the DjangoLANGUAGESsetting but has no entries inSplocalecontaineritemfor the collection's discipline, the fallback chain SHALL apply, ultimately falling back to the internal (untranslated) field name.Backend Implementation
Localization data SHALL be sourced from the same
Splocalecontainer/Splocalecontaineritem/Splocaleitemstrtables used byschema_localization.pytoday. The implementation SHALL reuse the existingget_schema_localization()function or its core logic to avoid duplicating the localization query logic.Field label lookup SHALL use a precomputed dictionary (keyed by
table_name→field_name) built once per request, not per object, to avoid N+1 database queries. The localization lookup SHALL be implemented as a post-processing step on the serialized data dictionary, not by modifying Django model serialization internals directly. The feature SHALL be opt-in via query parameter — no new URL routes are required.Caching headers SHALL include
Vary: Accept-Languageor appropriate cache key variation so that CDN/proxy caches do not serve localized responses for the wrong language.Performance Constraints
For a collection response of 100 objects, the overhead of adding
_fieldLabelsSHALL add no more than 50ms of server processing time compared to the same request withoutfieldLabels. The localization data for a single discipline SHALL be fetched in at most 2 database queries (containers + items), regardless of the number of objects in the response.Compatibility
The
fieldLabelsparameter SHALL be documented in the OpenAPI schema for all data endpoints. The internal field names used as JSON keys SHALL NOT change — this is a purely additive feature. All existing tests SHALL continue to pass without modification. The_fieldLabelskey SHALL be prefixed with underscore to indicate it is metadata, following the existing convention (resource_uri).Security and Access Control
Field labels SHALL respect the same collection access controls as the data — users can only see labels for fields they have permission to read. Hidden fields (
ishidden=truein schema config) SHALL still have their labels returned in_fieldLabelsif the user can see the field value —_fieldLabelsreflects what is in the response, not what is configured as hidden. No new database write operations are introduced — this feature is read-only for the localization tables.Error Handling
If the
fieldLabelsparameter has an invalid format (not matchingll[-cc]), the endpoint SHALL return a400 Bad Requestwith a descriptive error message. If the schema localization data cannot be fetched (e.g., database error), the endpoint SHALL still return the data response but omit_fieldLabelsand log a warning — data retrieval MUST NOT fail because of localization failures. If a particular field has no localization entry (field exists in Django model but not insplocalecontaineritem),_fieldLabelsSHALL use the internal field name as the label with no error.Example Workflows
Current (Two Requests):
New (One Request):
Collection Browsing:
Response includes
_fieldLabelsonce at the top level — the consuming application can label all columns in a table view without per-object overhead.Acceptance Criteria
GET /api/specify/collectionobject/<id>/withoutfieldLabelsreturns the same JSON structure as before this feature was implementedGET /api/specify/collectionobject/<id>/?fieldLabels=frreturns a response with_fieldLabelscontaining French field labelsGET /api/specify/collectionobject/?fieldLabels=en&limit=10returns_fieldLabelsat the top level (not inside each object)CollectionObjectAttribute), its fields have their own nested_fieldLabelsGET /api/specify/collectionobject/<id>/?fieldLabels=zz(nonexistent language) falls back to English labelsfieldLabelsdoes not affect POST/PUT request parsing — only the response bodyfieldLabelsis within 50ms of the same request withoutfieldLabels/context/api_endpoints.jsonDependencies
specifyweb/backend/context/schema_localization.py—get_schema_localization()functionspecifyweb/specify/api/serializers.py—_obj_to_data()and related serialization functionsspecifyweb/specify/api/crud.py— GET resource/collection handlersspecifyweb/specify/models.py—Splocalecontainer,Splocalecontaineritem,SplocaleitemstrDjango modelsspecifyweb/backend/context/views.py— OpenAPI schema generation for endpointsOpen Questions
_fieldLabelsbe included in PATCH responses? PATCH typically returns 204 No Content or the updated fields only. Recommendation: Include_fieldLabelsif a response body is returned.fieldLabels=*option to request labels in the user's currently authenticated language? This would avoid the client needing to track language separately. The server knows the authenticated user's language from the session/cookie._fieldLabelsinclude thedesc(description) field from the schema localization in addition toname? This would require a nested object like"_fieldLabels": {"catalogNumber": {"name": "Numéro de catalogue", "desc": "Le numéro de catalogue unique"}}instead of a flat string mapping. This adds complexity but may be valuable for UI tooltips.fieldLabelson the/api/specify_rows/query endpoint? This endpoint already returns column names — it may benefit significantly from localization support.Implementation Notes
_fieldLabelsconstruction should happen as a post-processing step after the full response dictionary is built, keeping it cleanly separated from the core serialization logic.annotate_field_labels(data: dict, model_name: str, language: str, country: str | None) -> dictthat can be called from the view/crud layer, or inspecifyweb/backend/context/schema_localization.py.Beta Was this translation helpful? Give feedback.
All reactions