How to handle nested resources with JSON HAL?
Asked Answered
P

1

8

Imagine a REST endpoint (/employees) serving pages of employees in JSON HAL format. An employee lives in a country, which resides in a continent.

For both countries and continents there are also separate endpoints.

The returned pages contain the typical _embedded field with the employee data. The employee resource also contains the nested country resource. This nested country resource also contains the _links.

In this case the output would be:

GET /employees

{
  "_embedded": {
    "employees": [{
        "employee_id": 1
        "name": "Mr. X",
        "place_name": "London",
        "country": {
          "alpha2_code": "AU",
          "name": "Australia",
          "continent": {
            "code": "OC",
            "name": "Australia",
            "_links": {
              "self": {
                "href": "http://localhost:8077/continents/au"
              }
            }
          },
          "_links": {
            "self": {
              "href": "http://localhost:8077/countries/au"
            }
          }
        },
        "_links": {
          "self": {
            "href": "http://localhost:8077/employees/1"
          }
        }
      },
      {
      ..
      }
    ]
  },
  "_links": {
    "first": {
      "href": "http://localhost:8077/employees?page=1&size=10"
    },
    "self": {
      "href": "http://localhost:8077/employees"
    },
    "next": {
      "href": "http://localhost:8077/employees?page=2&size=10"
    },
    "last": {
      "href": "http://localhost:8077/employees?page=8&size=10"
    }
  },
  "page": {
    "size": 10,
    "total_elements": 71,
    "total_pages": 8,
    "number": 0
  }
}

Is the nesting of the country (and also the nesting of continent within the country outputted in the correct way following the HAL specification.

In some other examples on the I noticed the following format:

{
  "_embedded": {
    "employees": [{
      "employee_id": 1
      "name": "Mr. X",
      "place_name": "London",
      "_embedded": {
        "country": {
          "alpha2_code": "AU",
          "name": "Australia",
          "_embedded": {
            "continent": {
              "code": "OC",
              "name": "Australia",
              "_links": {
                "self": {
                  "href": "http://localhost:8077/continents/au"
                }
              }
            },
          }
          "_links": {
            "self": {
              "href": "http://localhost:8077/countries/au"
            }
          }
        }
      },
      "_links": {
        "self": {
          "href": "http://localhost:8077/employees/1"
        }
      }
    },
    {
    ..
    }
    ]
  },
  "_links": {
    "first": {
      "href": "http://localhost:8077/employees?page=1&size=10"
    },
    "self": {
      "href": "http://localhost:8077/employees"
    },
    "next": {
      "href": "http://localhost:8077/employees?page=2&size=10"
    },
    "last": {
      "href": "http://localhost:8077/employees?page=8&size=10"
    }
  },
  "page": {
    "size": 10,
    "total_elements": 71,
    "total_pages": 8,
    "number": 0
  }
}

UPDATED: second example now also clearly shows it is a paged response.

It uses nested _embedded resources.

Is there - in perspective of the specification - one approach better then the other? Or are the both valid?

Philippeville answered 22/12, 2017 at 12:37 Comment(0)
C
12

Actually the HAL spec is pretty clear about when to use _embedded:

Embedded Resources MAY be a full, partial, or inconsistent version of the representation served from the target URI.

This has two implications:

  1. The nested document that's supposed to appear under _embedded also needs to be a representation of a linkable resource, i.e. it needs to be a resource on its own.

    The nested document placed in _embedded is considered a preview of the actual resource. Unless there's a dedicated resource for the nested document, don't put it in _embedded. If you're inclined to add a self link to the nested document, it needs to / should go into _embedded.

  2. There's usually a connection between the keys used within _embedded and a link that appears in _links of the same document.

An example

Take the following document representing an order as example:

{
  "_links" : {
    "self" : …,
    "customer" : …
  },
  "items" : [
    {
      "amount" : …,
      "description" : …,
      "_links" : {
        "product" : …
      }
      "_embedded" : {
        "product" : { … }
      }
    }
  ],
  "createdDate" : …,
  "_embedded" : {
    "customer" : {
      "firstname" : …,
      "lastname" : …
    }
  }
}

See how items is an array of potentially complex object directly nested in the document. That means there's no separate resource representing the items. They're part of this resource.

customer on the other hand appears both in the _links section, indicating there's a resource related to this one, whose semantics are defined by what customer means in the application domain. That same customer also appearing in _embedded basically indicates: here's a preview of what the representation of the related resource looks like. The nested document can be completely identical to what you'd get if you followed the link. but it can also be of completely different shape to serve the clients needs accessing the current resource. E.g. instead of listing firstname and lastname separately, the embedded variant could only contain a displayName, or a simple string version of an address that's a complex object in the actual resource's representation.

The same applies to the product nested inside the line item representation. The item might even have the description persistently derived from the state of the product it was added. But what's listed in items.[0]._embedded.product could essentially carry more in-depth information about the product the line item is pointing to. However, of course, the product is not "contained" in the line item.

This approach enables what's described in the spec as Hypertext Cache Pattern. The client inspecting _embedded.$rel.$interestingProperty first and -- in case it's not finding it -- resorts to resolving the link and looking for $interestingProperty there. That's a pretty standard procedure to implement and allows a server to gradually move properties into _embedded to avoid the clients to need to lookup the related resource in the first place. John Moore demonstrates this approach in this talk (using HTML as media type but effectively the same pattern).

The DDD side of things

Although REST -- and even more so HAL -- doesn't know anything about DDD, this distinction is pretty helpful when it comes to designing representations of DDD aggregates, as it allows to differentiate between nested, complex objects that are part of the aggregate (the line items in my example) and references to related aggregates (the customer in my example). The primary means to implement the latter are links of course, but very often you need to have access to a preview of the related resource (e.g. a master detail view on all orders for which you'd like to display the full name of the customer that placed the order). The concept of _embedded allows you to express exactly that.

What also plays into this is the question of what you actually update if you PUT a payload back to the server. Naturally you want to limit changes made to the resource to the aggregate that's backing it and not span multiple ones. In my example that means you naturally wouldn't want to be able to at the same time change details about the order and change the customer's lastname as that change would span both aggregates, which you're supposed to avoid according to DDD. By moving the customer related data into the mediatype-owned _embedded, the server can basically ignore the synthetic fields and only apply changes made to the natural ones.

Cotyledon answered 29/12, 2017 at 9:59 Comment(3)
Thanks for you very detailed answer Oliver.Philippeville
Looking at my original question - and taking the Hypermedia Cache Pattern into account - the multiple nested _embedded objects is the solution most close to the spec I think, correct?Philippeville
Yes, I think so.Cotyledon

© 2022 - 2024 — McMap. All rights reserved.