Enhancing OpenAPI Documentation Generation
Introduction
An OpenMRS server provides web-based APIs for querying and updating medical data. In O3, the frontend relies heavily on these APIs to fetch / modify data from the server. There has been some effort here and here to document these API, but they are generally incomplete and unreliable. As a result, frontend developers often need to directly look at the backend Java code to understand what APIs are available, how they behave, and what inputs and outputs are expected.
OpenAPI (formally Swagger) is an open standard for documentations that are both human and machine readable. There had been efforts to use OpenAPI to document OpenMRS web-based APIs, but again, they are under-maintained and are not reliable enough for consumption. Having OpenAPI documentations that are complete and accurate will be a huge win. Besides giving developers an easy way to understand the API endpoints, it can be used to automatically generate API clients for the frontend, greatly reducing frontend boilerplate code.
Goals
Rewrite our existing reflection-based tool to auto-generate OpenAPI documentation
Our existing tool makes incorrect assumptions about our APIs, leading to inaccurate and incomplete documentations.
OpenMRS server code uses the Spring framework, however its REST APIs are custom built. While there are existing tools out there that auto-generate OpenAPI documentations for Spring servers, they do not work for us.
Our REST APIs has a GraphQL-like feature that allows caller to specify subsets of fields for a given REST Resource. (This is know as
rep
or representation in our code base). Our existing reflection tool does not enumerate all the available fields for a REST Resource, and the new tool should be able to do that.Besides OpenMRS REST APIs, there are other APIs that are not documented with the existing tool. Particularly APIs made with Spring
Controllers
and FHIR endpoints.
Refactor our existing APIs so their (input and output) typings can be better documented
Some of our APIs essentially use HashMaps of key-value pairs as input and output types. They will need to be rewritten as properly typed java objects.
Some of our REST APIs (especially search) behave different depending on the inputs (similar to how 2 overloaded functions can have the same name but do different things). We should separate them into different APIs without breaking backwards comparability.
Include human-written Javadocs in the generated OpenAPI documenation.
This will allow us to consolidate documentation into one place, and deprecate much of https://rest.openmrs.org .
Make the generation of OpenAPI documentation happen at compile time rather than run-time.
This will allows us to easily write toolings to auto-generate frontend API clients.
Skills Required
Familiarity with Java. The following will be a plus, but not required:
Some knowledge of Java’s Reflection library
Some knowledge of the Spring Framework
Some knowledge of Maven and writing maven goals
This project can potentially involve a good deal of refactoring existing code base. Some experience with Agentic AI tools could be useful
Technical Details
High Level Overview of REST Request Handling
When a GET request is sent to a REST endpoint (ex: /ws/rest/v1/patient/<uuid>?v=<rep>
), the following happens:
MainResorceController handles the request, determines which resource the request is for (PatientResource), and calls that resource's
retrieve()
function.DelegatingCrudResource (which is the superclass of most *Resource.java files) provides the retrieve function, which calls
asRepresentation()
to that reads the <rep> parameterasRepresentation()
attempts to convert the <rep> to a list of Patient properties (fields) to be retrieved using a combination of different strategies:PatientResource.getRepresentationDescription()
is first called. This function (usually) lists out the properties that should be retrieved for the "standard" representations commons to most resources (default
,full
andref
).if the above function returns
null
, thefindAnnotatedMethodForRepresentation
function is called to find the method in the *Resource.java class that is annotated with@RepHandler
to handle the specified<rep>
.If the above fails, it assumes that the <rep> is a customer representation (ex:
custom:(uuid)
), and callsgetCustomRepresentationDescription
to retrieve the list of properties.
The BaseDelegatingConverter's
convertDelegateToRepresentation()
function gets called to loop through each property name and converts it to actual values by callingDelegatingResourceDescription.Property.evaluate()
The
evaluate()
function tries a combination strategy of reflection and annotated methods to retrieve the value of the property. This process can be recursive as the property can itself be another Resource.The evaluated object gets returned by
MainResourceController.retrieve()
to be serialized into the appropriate data format (xml or json)
Challenges
current documentations are in 2 places: we have OpenAPI docs are generated from the backend, and we also have rest.openmrs.org. We should be able to merge them
rest.openmrs.org can have narrative explanations that are useful in addition to normal javadocs. However, its shell / java / javascript code examples are probably better represented / maintained in OpenAPI.
It should be possible to include javadocs in the generated OpenAPI docs, see this. This should reduce the need to document things in multiple places
We only have OpenAPI documentation for REST Resources, but we have other web APIs (like controllers) that need to be documented.
Example: Appointments have a REST-like API, but is actually implemented as a Controller, so it doesn't show in the OpenAPI docs.
EMRAPI contains web APIs that are just not REST-like at all. Those need to be documented as well.
Most REST resources have "default", "full", and "ref" representation, but it is possible to have more via "NamedRepresentaton". Examples:
ConceptResource1_8.java:
@RepHandler(value = NamedRepresentation.class, name = "fullchildren")
AdmissionLocationResource.java:
if ((rep instanceof NamedRepresentation) && rep.getRepresentation().equals("layout")
There does not seem to be an easy way to get what named representation is supported by a Resource
We need to inspect the logic of the
getRepresentationDescription
function, and also any function annotated with@RepHandler
REST resources can also have "custom" representations. It would be nice to document what properties can be included, but that's difficult. It is possible to have properties that are not in "full" representation. Example:
The
Patient
class extends thePerson
class, andPatientResource
allows for getting fields from thePerson
object as if it's from thePatient
object. These properties are not documented in the Patient full representation, but they can be specified in custom rep. For example, theattributes
anddead
fields actually belong in thePerson
class, but you can also retrieve them from the Patient resource:There can be "alias" properties. For example, the
Person
resource haspersonName
which is an alias topreferredName
:
We use
getRepresentationDescription()
to define the fields that get returned for a given resource's representation, but we usegetGetModel()
to document what fields are in a representation. This can lead to inconsistencies. Can we combine them?Similarly, can we use
getCreatableProperties()
to replacegetCREATEModel()
, andgetUpdatableProperties()
to replacegetUPDATEModel()
?PR Where
getGETModel()
is introduced: RESTWS-562 Improve Resource Definition Documentation by gayanW · Pull Request #288 · openmrs/openmrs-module-webservices.restomod-common/src/main/java/org/openmrs/module/webservices/rest/web/resource/impl/DelegatingResourceHandler.java
With our current implementation, there is no way to type search parameters. For example, ObsResource1_8.java has a
doSearch()
function that takes in aRequestContext
, and from that we extract the paramspatient
,encounter
andq
. Is it possible to re-architect the search function to take in a better typed object?There are also resources that have different search implementations depending on which params are passed in (See ObsResource1_8.java, VisitResorce1_9.java). For those, it might make sense to have different search functions (with different input types) instead of trying to cramp the logic into one function.
OpenAPI 2.0 does not support
oneOf()
types, but upgrading to 3.0 should help. Where that can be useful:when we do
/patient?v=default
, we should returnPatientGetDefault
, when we do /patient?v=full, we should returnPatientGetFull
v=
parameter can be oneOf(enum('ref', 'default', 'full'), string)