8

I have an API endpoint that returns some statistics. Currently the response looks like:

Option 1:

{
    "stats": [
                {
                    "name": "some-stats-key1",
                    "value": 10
                },
                {
                    "name": "some-stats-key2",
                    "value": 20
                }
            ],
    ... other keys
}

But this looks a bit complex and I what to make it like:

Option 2:

{
    "stats": {
                "some-stats-key1": 10,
                "some-stats-key2": 20
            }
    ... other keys
}

I understand that Option 1 is easier to be extended, but less comfortable for users. What other issues I can face using one of these options? Or should I make a hybrid solution like:

Option 3:

{
    "stats": {
                "some-stats-key1": {
                                        "name": "some-stats-key1",
                                        "value": 10
                                    },
                "some-stats-key2": {
                                        "name": "some-stats-key2",
                                        "value": 20
                                    },
            },
    ... other keys
}

The keys "some-stats-key1" and "some-stats-key2" are just internal values and it's expected API user will map them into readable names using documentation. All the keys are unique.

The order of "stats" isn't important.

The typical use case is just to get all the stats, match keys with readable names and show as a table on a web-page. But currently I can't say if no one will need only a part of the stats later.

Is there a best practice for this issue?

  • 1
    Why exactly do you have the name "stats" explicitly named inside the response? If you are not responding with anything other than "stats" then drop the outer wrapper and just return the array of key-value pairs. That might get you closer to the cleanliness you seek. – K. Alan Bates Jul 4 '17 at 11:54
  • 5
    Consider how your clients are going to deserialise your JSON into a structure. For example, if you're consuming this API from a webpage over AJAX, then option 1 allows you to easily iterate through the key-value pairs with Array.forEach and other Array functions. Your other examples will add extra complexity to array-like operations, which could just be making life difficult for your clients – Ben Cottrell Jul 4 '17 at 12:00
  • @K.AlanBates stats is not the only key in a response. Updated. Thank you. – Yann Jul 4 '17 at 12:11
  • 1
    Is order of the entries important? Option 1 preserves the order. – Roman Susi Jul 4 '17 at 12:11
  • 2
    Without hinting at most frequent API use cases, there could be no best practice answer here. – Roman Susi Jul 4 '17 at 12:15
8

I'd go for option 2. If the API consumer will convert some-stats-key1 into something readable, that probably means he/she has a list of values he/she is interested in (say, some-stats-key1 and some-stats-key3), and will iterate over that list. By choosing a JSON object, it will be deserialized as a dictionary/map which provides a convenient lookup for the consumer of the API.

This will be more cumbersome with option 1, where the consumer needs to iterate over the JSON array, or pre-create their own dictionary with interesting keys.

Option 3 is a little too verbose for me, the duplication of the key names just doesn't appeal to me.

If extensibility is a concern, you can always publish a v2 of your API returning something like

"stats": {
    "some-stats-key1": { "value": 10, "error-margin": 0.1 },
    "some-stats-key2": { "value": 20, "error-margin": 0.2 }
}

and keep the v1 for backwards compatibility. Maintaining backwards compatibility within a single version of the API can be a real PITA if you don't have complete control over how the API is consumed. I've seen a consumption of an API of mine 'break' when I just added an extra (optional) key-value pair (i.e. not changing the structure).

  • 2
    -1 for "you can always publish a v2 of your API". – Eric Stein Jul 5 '17 at 1:36
  • @EricStein thanks for taking the time to explain your downvote. I edited my post to indicate why I think this is the best option. If you still disagree, fine with me. – Glorfindel Jul 5 '17 at 6:55
  • 1
    Versioning a public-facing API is not trivial or to be done lightly. I think your answer handwaves that away as an argument in favor of your preferred approach. – Eric Stein Jul 5 '17 at 14:32
  • IMHO, it's (far) easier than maintaining backwards compatibility with a single version. – Glorfindel Jul 5 '17 at 14:33
  • Returning two versions of the same resource type in the same response is probably the worse idea someone can have working with REST APIs :-/. Is that what you are suggesting in your example? – Laiv Jul 5 '17 at 17:44
6

The two options have the classic List vs. Map advantages.

1) The List allows duplicate entries and maintains order. If these features are important, use the List, even though it is clunkier.

2) The Map does not allow duplicates. Maintaining order is possible with a little extra work. The big advantage is more simplicity in the data format, and searching for a particular element is trivial.

My default choice is always the simpler Map, but YMMV.

3

When I get data from an API, I always check that everything is as I expect it. So my effort to process your data consists of the verification, and the actual processing.

In case 1 I have to check: a. There is an array. b. All items in the array are dictionaries. c. Every dictionary has a key "name". d. All values for the "name" key are unique.

In case 3 I have to check: a. There is a dictionary. b. All values in the dictionary are dictionaries. c. Each dictionary has a key "name" with a value that matches the key in the outer dictionary. Slightly better.

In case 2 I have to check: a. There is a dictionary.

(Of course I have to check the values as well). So your case 2 requires the least amount of checking on my side. I actually get a data structure that is immediately usable.

The only problem with 2 is that it is not extendible. So instead of sending the value as a number, you might send { value: 10 } which can then be extended in a backward compatible way.

Redundancy is bad. The only thing that redundancy achieves is to make me write more code, and forcing me to think about what I should do if the redundant bits don't agree. Version 2 has no redundancy.

1

Since you asked of good practices for API design:

  • I never return an api response containing an object at the top level. All service calls return a set (as an array), and that set contains elements (as object instances.) The number of objects returned in the array is irrelevant to the server. If the client needs to make a determination from the number of items returned in the response, that burden is the client's to enforce. If a single item is expected, it gets returned as a unit set
  • When I'm designing an api, I don't presume to know what specific technology my api clients will be using to consume said api, even when I know what specific technology they are most likely to use. My responsibility is to communicate my domain representations consistently to all consumers, not conveniently to a particular consumer.
  • If a structural option exists to allow the response to be composed, this structure takes priority over options which do not
  • I strive to avoid creating representations that have unpredictable structures.
  • If the structure of a representation can be predicted, then all instances inside the response set should be of the same representation and the response set should be internally consistent.

So, given the structures you've proposed, the structure I would implement would look something similar to this

[
   { /* returns only the 'common' metrics */
      "stats": [
                  {"key":"some-metric1","value":10},
                  {"key":"some-metric2","value":20}
               ]
   },
   { /* returns an optional metric in addition to the "common" metrics */
      "stats": [
                  {"key":"some-metric1","value":15},
                  {"key":"some-metric2","value":5},
                  {"key":"some-optional-metric", "value":42}
               ]
   },
   { /*returns the 'common' metrics as well as 2 candidates for "foo-bar" */
      "stats": [
                  {"key":"some-metric1", "value": 5},
                  {"key":"some-metric2", "value": 10},
                  {"key":"foo-bar-candidate", "value": 7},
                  {"key":"foo-bar-candidate", "value": 11}
               ]
   }
]
  • Could you elaborate on why you follow point 1, perhaps citing a reference or example from a framework or some well known web APIs? Overall your answer looks overly complex, at least at first glance. – user949300 Jul 6 '17 at 5:30
  • @user949300 ...hmmm. re:overly complex looks dead simple to me. I follow point #1 because it causes all serialization logic to maintain consistent conventions. The number of items in a response set is an implementation detail of the service interface. To communicate "3" items in a set, those three items are most conveniently wrapped in an array. "1" is a number. – K. Alan Bates Jul 6 '17 at 13:16
  • @user94900 re: example from a well known web API google.com – K. Alan Bates Jul 6 '17 at 13:59
  • @user94900 re: example from a framework anything that speaks ANSI SQL – K. Alan Bates Jul 6 '17 at 14:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.