There has been a lot discussion on the Skype Implementers channel and other forums about FHIR Transactions, specifically the rules around HTTP POST
and the use of Bundle.entry.fullUrl
and Bundle.entry.resource.{resourceType}.id
, and how servers should behave.
Readers who want to reference the relevant portions of the FHIR DSTU2 specification may refer to the Bundle resource, and the explanation of transactions.
In a FHIR POST
, a resource may not contain an id
element, as only a server may assign an id
. In a FHIR PUT
, the client should supply an id
within the resource.
This is correct:
HTTP POST [base]/Patient { "resourceType": "Patient", "name": [{ "family": ["Doe"], "given": ["John","James"] }], "gender": "male", "birthDate": "2016-01-01" }
This is not allowed (notice the addition of an id):
HTTP POST [base]/Patient { "resourceType": "Patient", "id": "1234567890", "name": [{ "family": ["Doe"], "given": ["John","James"] }], "gender": "male", "birthDate": "2016-01-01" }
In a FHIR transaction, each Bundle.entry
may contain a single FHIR operation. For example, a POST
or PUT
(and so forth). If the operation is a POST
, it seems logical that the client should not be able to provide an id
within the resource (traditionally, a server generates the id
on a create operation in a RESTful architecture).
But this has two issues when dealing with transactions.
First, when a resource
is provided within a Bundle.entry
, a fullUrl
is required. See the following invariant rule:
(not(exists(f:fullUrl)) and not(exists(f:resource))) or (exists(f:fullUrl) and exists(f:resource)))
Second, the transaction may contain several resources that need to be linked together. For example, transferring a patient record (in this case, a simple example where there is a Patient
with one Observation
). Normally, the Patient
requires an id
before it can be referenced by an Observation
. The specification, particularly this example (without resource linking), suggests that we can link resources together using the Bundle.entry.fullUrl
rather than the Bundle.entry.resource.{resourceType}.id
. For example:
HTTP POST [base] { "resourceType": "Bundle", "type": "transaction", "entry": [{ "fullUrl": "urn:uuid:9e33a01d-c35f-4920-9179-50d5b8a6f992", "resource": { "resourceType": "Patient", "name": [{ "family": ["Doe"], "given": ["John","James"] }], "gender": "male", "birthDate": "2016-01-01" }, "request": { "method": "POST", "url": "[base]/Patient" } },{ "fullUrl": "urn:uuid:8a840627-2b30-42af-8a20-b6eea1e307b3", "resource": { "resourceType":"Observation", "status":"final", "code":{"coding":[{"system":"http://loinc.org","code":"8302-2","display":"height"}]}, "subject":{"reference":"Patient/9e33a01d-c35f-4920-9179-50d5b8a6f992"}, "valueQuantity":{"value":163.068,"unit":"cm","system":"http://unitsofmeasure.org","code":"cm"} } }, "request": { "method": "POST", "url": "[base]/Observation" } }] }
In this case, the fullUrl
is required for each Bundle.entry
(lines 6,18) because a resource
is present (see the invariant rule). Normally, the Patient
would also have an id
because the Observation
needs to reference the Patient
, but in this case the Patient
is referenced by urn:uuid
(line 6) within the Observation
(line 23).
Reading the FHIR documentation on the subject, which is spread out across two pages (Bundles and Transactions), contains language which various folks have had difficulty interpreting.
However, I think the above example is recognized as a valid use-case, and should be supported. The result being that server-side, the server continues to maintain its ability to generate the resource id
values and it can maintain the resource graph (e.g. the Observation
remains linked to the Patient
).
Transferring a Record: $everything && transaction
The previous example is a pretty basic transaction, where the client is transferring a new patient and their associated data to another FHIR server. Let’s look at a slightly more complicated example:
- FHIR Client reads a
Patient
record using the$everything
operation. - FHIR Client transfers that
Patient
record to another FHIR server using a single transaction.
In this scenario, the client should issue a GET
request for the patient (the specification allows for POST
to $everything
as well), read the HTTP response body, and turn around and POST
the response body to the other FHIR server.
1. Fetch Patient Record
HTTP GET [base]/Patient/1234567890/$everything HTTP/1.0 200 OK Content-Type: application/json+fhir; charset=UTF-8 { "resourceType": "Bundle", "id": "everything-response", "type": "searchset", "entry": [{ "fullUrl": "[base]/Patient/1234567890", "resource": { "resourceType": "Patient", "id": "1234567890", "name": [{ "family": ["Doe"], "given": ["John","James"] }], "gender": "male", "birthDate": "2016-01-01" } },{ "fullUrl": "[base]/Observation/1", "resource": { "resourceType":"Observation", "id": "1", "status":"final", "code":{"coding":[{"system":"http://loinc.org","code":"8302-2","display":"height"}]}, "subject":{"reference":"Patient/1234567890"}, "valueQuantity":{"value":163.068,"unit":"cm","system":"http://unitsofmeasure.org","code":"cm"} } } }] }
2. Transfer Response Body as Transaction
The client just retrieved the entire Patient
record. Without having to edit the response body JSON or XML at all, they should be able to turn around and POST
that response as the body of a new transaction. This means no changes to:
Bundle.type
Bundle.entry.fullUrl
Bundle.entry.resource.{resourceType}.id
Bundle.entry.request
The difficulty here is that the transaction requires a request
element for every Bundle.entry
, which is not present in the $everything
response.
One strategy is for the client to create a request
for each Bundle.entry
. Perhaps as a conditional create, or conditional update (both of which might require special logic on the client-side to generate the appropriate conditional search parameters), or just as an old-fashioned POST
. This seems like the safest approach, but does require a lot of client-side work on creating the conditional logic.
A second strategy is for the server to make some assumptions about the request. The specification does allow support for this scenario, but does not mandate it. Specifically, see the accepting other bundle types paragraph documented within the transaction definition. Basically, in this case the server chooses to use a POST
or PUT
for each Bundle.entry.resource
depending on whether or not the server recognizes the each Bundle.entry.resource.{resourceType}.id
value or not (actually, it is NOT explicit about id
value matching, instead it says “identity” which is open to interpretation).
At first glance, this seems like a great capability. However, it is potentially dangerous and destructive, so I suggest its inclusion within the specification be carefully reconsidered (or possibly just clarified if my interpretation is incorrect). These are medical records we’re talking about, not posting a new blog article or comment. If the server decides that it should use PUT
instead of POST
because it already has a resource of that type with the same id
— how does it know that the new representation is actually an updated version of the original rather than something else entirely? You could easily end up with a client (or another server) creating the same id
for something that is already on a different server and then overwriting it by mistake. This is the original reason for server assigned id
values.
If there is a server-side option, it probably should be much more careful than just examining resource id
values. First, it should explicitly know that it just received a transaction of resources for transfer. I recommend that we create a new Bundle.type
code called transfer
. At a minimum, the client would change the Bundle.type
from Step #1, and now the server has some context. Rather than just match on id
values, it can now do something more intelligent — using whatever patient matching algorithms it supports. With a transfer
, the server could optionally save the current id
values as identifier
values when they are persisted. Another option, is the creation of a new $transfer
operation, since we’re talking about advanced logic here, beyond simple transaction CRUD operations.
Please share your thoughts and comments below.