Proposal: Changes to REST module to improve typings

I want to propose a few changes to the REST module. Most of these are motivated by trying to improve the typings and documentations of our REST APIs, although some of them are miscellaneous. I don’t think any of the proposed changes breaks backwards compatibility.

Explicitly specify search handler id in URL when making search requests

Almost every REST resource provides a route to do a search. For example GET /ws/rest/v1/encounter?patient=<uuid> searches for encounters by patient uuid. We want to better document what params are accepted as search parameters, but this turns out to be difficult.

  • For a resource, it is possible to have multiple search handlers, and each search chandler can accept different parameters. A search handler is selected either by:
    • explicitly passing in a s= GET param to specify the search handler id. For example, GET /ws/rest/v1/encounter?patient=<uuid>8&obsConcept=<uuid>&s=byObs invokes the search handler with id byObs
    • implicitly infer which search handler to use based on the params that are passed in. For example, GET /ws/rest/v1/encounter?patient=<uuid>8&obsConcept=<uuid> will cause the REST module to do a best guess and find a search handler that handles the parameters patient and obsConcept. In this case, this also happens to be the byObs search handler.
  • It is possible that NO search handlers are selected based on the params passed in. In that case, some REST resources have their own search() function (via the Searchable interface), and that gets invoked. (Essentially this Searchable#search() function plays a similar role to SearchHandler, but it’s just not implemented as such). For example, GET /ws/rest/v1/encounter?patient=<uuid> fails the param criteria of the Encounter resources’ search handlers, but invokes Encounter.search() as a fallback.

The above logic is coded here.

I proposed that:

  • we add these routes to support explicitly selecting search handlers. This allows us to document which search handlers are available, and what parameters each search handler expects.
  • we make these routes available as POST requests in addition to GET. This doesn’t help with typings, but it solves the problem of URL character limits we run into when our GET params are too long, especially when we provide long v=custom:... strings. (FHIR does something similar)

So the new routes would look something like this:

  • GET /ws/rest/<resource>/search/ selects the SearchHandler with id "default"
  • POST /ws/rest/<resource>/search/ (same as GET)
  • GET /ws/rest/<resource>/search/<searchHandlerid> selects the SearchHandler with the given searchHandlerId
  • POST /ws/rest/<resource>/search/<searchHandlerid> (same as GET)

SearchHandler, compared to Searchable#search(), has an additional advantage of needing to declare its required and optional parameters (example). While we still need to inspect the code to determine the typing of each parameter, they are easier to document that Searchable#search() (see this for comparison). Overtime, I think we should rewrite our REST resources to favor SearchHandlers over the search() / doSearch() functions in the Resource classes.

Explicitly return the representation of a resource.

When we fetch a resource, we can include a v= parameter specify its representation. For most resources, we can do something simple, like v=default, v=full, or provide a custom representation string like v=custom:(uuid,display). It is often the case that the returned resource transitively include other resources as well. For example, when fetching a Visit resource with default rep, it also transitively returns its patient, location, and encounters. Here is a sample (some fields omitted):

{
  "uuid": "c601d849-7f3a-4e8f-b006-4556887013ff",
  "display": "Home Visit @ Site 42 - 03/09/2025 06:19 PM",
  "patient": {
    "uuid": "1336ec3e-7ea9-4883-8a5b-f0332b9e84d8",
    "display": "10001F0 - Sandra Walker",
  },
  "visitType": {
    "uuid": "d66e9fe0-7d51-4801-a550-5d462ad1c944",
    "display": "Home Visit",
  },
  "indication": null,
  "location": {
    "uuid": "92dbdbdf-17da-4cf0-873c-ad15dfae71cb",
    "display": "Site 42",
  },
  "startDatetime": "2025-03-09T18:19:21.000+0000",
  "stopDatetime": "2025-03-09T18:41:21.000+0000",
  "encounters": [
    {
      "uuid": "f616575b-2415-49df-9c57-e915f238dffa",
      "display": "Lab Results 03/09/2025",
    },
  ],
  "resourceVersion": "1.9"
}

Currently, our documentation is not mature enough to specify the presentation for the transitively fetched resources. (Most of them should be of ref representation, but there is no guarantee.) I proposed that, for each resource, we explicitly include an extra field to specify its representation. This allows us to better discern the typings of the resources that are returned. For example, the above JSON would be augmented to this:

{
  "uuid": "c601d849-7f3a-4e8f-b006-4556887013ff",
  "display": "Home Visit @ Site 42 - 03/09/2025 06:19 PM",
  "rep": "default", // could also be "full", "custom" or other named representations
  "patient": {
    "uuid": "1336ec3e-7ea9-4883-8a5b-f0332b9e84d8",
    "display": "10001F0 - Sandra Walker",
    "rep": "ref"
  },
  "visitType": {
    "uuid": "d66e9fe0-7d51-4801-a550-5d462ad1c944",
    "display": "Home Visit",
    "rep": "ref"
  },
  "indication": null,
  "location": {
    "uuid": "92dbdbdf-17da-4cf0-873c-ad15dfae71cb",
    "display": "Site 42",
    "rep": "ref"
  },
  "startDatetime": "2025-03-09T18:19:21.000+0000",
  "stopDatetime": "2025-03-09T18:41:21.000+0000",
  "encounters": [
    {
      "uuid": "f616575b-2415-49df-9c57-e915f238dffa",
      "display": "Lab Results 03/09/2025",
      "rep": "ref"
    },
  ],
  "resourceVersion": "1.9"
}

This will bloat the returns result by a bit (< 20 bytes per resource), but this should small enough to be acceptable.

Allow specifying returned data format (XML vs JSON) as a GET / POST param

Currently, the data format returned by the REST module is specified using the "Content-Type" HTTP request header. A request with "Content-Type: application/json; will return JSON data as response. If the header is not specified, it returns XML by default.

I propose that we add a new parameter format= in our GET / POST request as an additional way to specify the returned data format. (FHIR does something similar with the _format param). This just mostly helps with development / debugging.

3 Likes

Had a discussion about this during the platform call today (thanks everyone for their feedback). Feedback from the meeting:

  • Explicitly specify search handler id in URL when making search requests

    • It probably makes sense to add those new routes to specify search handlers

      GET /ws/rest/<resource>/search/
      GET /ws/rest/<resource>/search/<searchHandlerId
      
    • It also probably makes sense to support searching via POST requests as well, although the URL for the POST requests need to differ from the GET routes slightly because we use the routes (but not also the HTTP methods) to determine the Controller to handle routing.

  • Explicitly return the representation of a resource.

    • The problem of not being knowing the representation (and therefore the fields) of child Resources returned should hopefully be fixed when we have better OpenAPI typings + documentation. Let’s not implement this for now.
  • Allow specifying returned data format (XML vs JSON) as a GET / POST param

    • It should be fine to add a param to specify the return data format. Maybe name it _format= to make it more FHIR-like.

One thing from above that I forgot to mention in the meeting was that I want push us to move away from implementing Resource#doSearch() in favor of using SearchHandlers instead (since Resource#doSearch() is even less type-strict than SearchHandlers). I’m interested in thoughts on that.

Thanks!

2 Likes

Some update: We had a GSoC project over the summer with @capernix to make a maven plugin to generate OpenAPI documentations at build time. While this project isn’t finished yet, it does give us a bit more understanding / clarity on what we want to change in the REST module itself. I want to propose the following breaking changes:

Remove getAvailableRepresentation() function in Retrievable interface

Each REST resource can implement 0 or more of these interfaces to define their various CRUD “abilities”: Retrievable, Searchable, Listable, Creatable, Updatable, Deletable, Purgable. In particular, Retrievable, Searchable and Listable denote support for GET operations, either by uuid, by search query params, or by no search query params at all. These GET operations support adding a v= param to specify the representation (datatype) of the returned value. However, Retrievable is the only interface that specifies the getAvailableRepresentations() function. This is incorrect, and the available representations should also be applicable to Searchable and Listable operations. (There are in fact resources that are Searchable and Listable, but not Retrievable, like ConceptSearchResource.)

Note that the getAvailableRepresentations() function does not actually make it easier for us to get all of the representations and determine their typings. That’s because, for a given Resource, its supported representations can either be defined in its getRepresentationDescription() function, or in functions with the @RepHandler annotation. Thus, we need to use reflection to loop through each method to find the ones with @RepHandler annotation anyway.

I propose that we deprecate the Retrievable#getAvailableRepresentations() function, and eventually remove it entirely. Instead, for each Resource, we use its getRepresentationDescription function and functions with @RepHandler annotation as source of truth for what representations it supports.

Allow @RepHandler methods to return any data type (not just SimpleObject)

A resource can declare support of a representation by either defining a list of property names for it in getRepresentationDescription(), or have a @RepHandler function that outright constructs the object to return. Currently, a @RepHandler function must have SimpleObject (essentually a Map) as its return type. This limits our ability to document the return type (and its properties) more precisely. I propose relaxing @RepHandler functions so that it can return any type (Object), as long as it is JSON-serializable. This allows us to write functions like this:

// We can see that the type it returns is `CustomPatient`, with its associated properties (not shown).
@RepHandler(name = "SomeRep")
public CustomPatient getPoint() {
  return new CustomPatient("John", "Doe", 21);
}

This also allows us to have @RepHandler functions with return types that needs to their own serialization logic, like FHIR resources:

@RepHandler(name = “fhir”) public org.hl7.fhir.r4.model.Patient getFHIRPatient() { PatientTranslator translator = …; Patient patient = …; return traslator.toFhirResource(patient); }

Make REST routes return JSON by default

By default, when we go to a browser’s address bar and go to <baseURL>/ws/v1/<resource>, we get data returned in XML, even though the response does not have Content-Type: application/xml header. In fact, +5 years ago, we made a change to explicitly forbid requests that specify Content-Type: application/xml for security reasons. However, we kept the response data in XML (even though the Content-Type is not specified as such), likely due to compatibility issues. I think we’ve waited long enough, and I propose that we just have the returned value be in JSON if no Content-Type is specified in the request.

cc: @ibacher @dkayiwa @mherman22 @mseaton @mogoodrich

2 Likes

There is a risk here. Part of the reason that we use SimpleObject everywhere is that it avoids the circular reference mess that we tend to get into when trying to directly map arbitrary OpenmrsObject classes to JSON using the ObjectMapper.

(I think this is a great idea, but we do need to solve being able to actually serialize non-Map objects before we can get to that).

@ruhanga Is this something your PR might help out with?

cc: @raff

Yes, in short @ibacher. This is one of those areas (de)serialization with Jackson could help with. However, achieving the full range of capabilities currently provided by the SimpleObject and converter logic would require some further careful configuration and fine-tuning of the Jackson library.

1 Like

If we’re explicitly using /ws/rest/v1/<resource>/search its unproblematic to support search-via-POST from that URL. The issue arises when search is done like /ws/rest/v1/<resource>/?q=<some search> because a POST to that URL is also usually how you create a new resource, so you effectively have one URL two with possible functions depending not just on the path + verb, but the path + verb + query string, which generally violates REST construction.

1 Like