Allow naming of root element when rendering response as XML?
Ben-Fenner opened this issue Β· comments
We have a client who insists on our API providing XML, so we're fulfilling that.
The XML root element name is hard-coded to data
in the Graphiti renderer which is not ideal since there is a child element inside also named data
.
We have worked around this, but it would be nice to be able to specify the root element name where the Graphiti renderer could use it.
Looking at the source, here is where the magic happens, best I can tell:
graphiti/lib/graphiti/renderer.rb
Line 37 in 0389e50
Right now this is a simplified version of what our work-around looks like:
# app/controllers/api/my_controller.rb
class Api::MyController < Api::ApplicationController
def index
resources = MyResource.all(params)
respond_to do |format|
format.json { render(json: resources) }
format.jsonapi { render(jsonapi: resources) }
format.xml { render(xml: pretty_xml(resources)) }
end
end
# By default when a Graphiti resource proxy list is resolved to XML it looks like this:
#
# <data>
# <data type="array">
# <datum>
# ...
# </datum>
# </data>
# <meta>
# ...
# </meta>
# </data>
#
#
# Instead we want it to look like this:
#
# <root>
# <data type="array">
# <datum>
# ...
# </datum>
# </data>
# <meta>
# ...
# </meta>
# </root>
def pretty_xml(resource_proxy)
json = resource_proxy.to_json
JSON.parse(json).to_xml(root: :root)
end
end
Not totally against it, but JSONAPI top-level key is data
. Isn't it the same issue? Intent is to be consistent.
I've been developing and testing this whole time using JSON (not JSONAPI) and then am only just now getting to work on the XML part. I haven't actually paid any attention to what the JSONAPI results look like.
Looking at it now, it seems there is no top-level key for the JSONAPI payload (same as with the JSON payload). There are two keys on equal level just like with JSON. The data
key and the meta
key.
(Although maybe things have changed in newer versions. I am stuck on Graphti 1.3.5 right now. Sorry!)
Example:
{"data":[{"id":"1","type":"my_resource","attributes":{"title":"Resource number 1"},{"id":"2","type":"my_resource","attributes":{"title":"Resource number 2"}],"meta":{"stats":{"total":{"count":4}}}}
When no count
is requested, the meta
key still exists in a JSONAPI response (while the JSON response omits it) but is just an empty hash.
It would appear that XML requires an additional, named, root layer, whereas the JSON and JSONAPI formats are happy enough to have an unnamed root container layer.
The other keys are meta/included, which aren't really relevant to the shape of JSON/XML, which leaves us with just data
. Isn't the top-level key for JSON responses data
right now? Again not opposed to it, but the consistency outweights a key named data in my personal opinion
As far as I can tell, there is no top-level key for JSON-formatted payloads, nor for JSONAPI-formatted payloads. And by that I mean, if a meta
key exists, it is at the same level as the data
key. When they both exist, there is no top-level key. It's just a hash with two equal-level keys. The top-level in this situation is essentially the hash itself, which JSON is happy to leave unnamed.
XML just wants to wrap that whole situation up into an additional named top-level key. So when there is only a data
key, you end up with two data
keys (one the parent of the other) as shown in the code comment in the original post.
Am I explaining this well enough? Maybe I need to draw some Venn diagrams? :)
Maybe if I format the example from above a bit you'll see what I mean.
Does this representation of the JSONAPI response help?
{ "data":[
{"id":"1","type":"my_resource","attributes":{"title":"Resource number 1"},
{"id":"2","type":"my_resource","attributes":{"title":"Resource number 2"}
]
,
"meta":{"stats":{"total":{"count":4}}}
}
Right, this is JSONAPI. But meta
isn't relevant for JSON/XML (included
is the other example). Put another way, vanilla JSON (not JSONAPI) has a top-level data
key, XML matches that same payload for consistency.
Hmmm, I'm not seeing that on my end.
Here is what the same response looks like when requested as JSON (and returned as JSON not JSONAPI):
{ "data":[
{"id":"1","title":"Resource number 1"},
{"id":"2","title":"Resource number 2"}
]
,
"meta":{"stats":{"total":{"count":4}}}
}
Haha, my mistake I didn't know we supported meta. Sorry! In any case, doesn't this match XML and JSONAPI? You want to rename data
, right?
JSON and JSONAPI match each other when it comes down to the relevant hierarchy. XML adds an additional layer wrapping everything. That additional wrapper just so happens to be called data
. It could be called root
and make a bit more sense, and be more like what XML consumers are expecting. But I mean, it could be called master_of_the_universe
and still not have any effect on the data
key you're hoping to keep unchanged.
To be clear, I am not trying to rename the data
key that is on the same level as the meta
and included
keys. Of course, that should remain untouched. This additional root-level wrapper that only exists for the XML representation is what I want to rename (and am doing so with my ugly work-around). It would be nice to not have to use this work-around.
Again, check out the large code comment above for what I'm hoping to accomplish.
Gotcha, I see now, thanks for bearing with me. Sounds like we want to remove that top-level key entirely then?
I'm not well-versed in XML, so I've looked this up. Apparently a root (top-level key) is required. So it can not be removed.
https://stackoverflow.com/questions/4304038/do-you-always-have-to-have-a-root-node-with-xml-xsd
The ideal situation IMO is to default the name of the top-level key to data
(as to not mess things up for anyone else currently relying on this gem and its existing behavior) but allow it to be changed if desired.
Something like this:
def to_xml(root = :data)
render(self.class.hash_renderer(@proxy)).to_xml(root: root)
end
How that would be handled upstream of the method is not something I've looked at, nor have I considered exactly what it would look like to send this new argument in the controller...
Ah, I thought there was a root: false
but apparently not. I think you've won me over :)
Thank you so much for your quick and informative replies. I keep forgetting to mention this. It's nice to have a good discussion with quick turn-around. π
If you're cool with potentially breaking implementations that rely on the XML formatting done by Graphiti, your easy button is:
def to_xml
render(self.class.hash_renderer(@proxy)).to_xml(root: :root)
end
Or if you're okay with the default name of hash
you can omit the setting entirely:
def to_xml
render(self.class.hash_renderer(@proxy)).to_xml
end
Update your test(s) and be done. π
I'm not sure this is the way you'd like to go, but it is a thought. π
No problem! Thanks for bearing with me. I was thinking for backwards-compat:
def to_xml
render(self.class.hash_renderer(@proxy)).to_xml(root: Graphiti.config.xml_root)
end
And defaulting the option to :root
Oh yah, a configuration seems like an elegant solution. For backwards-compatibility you'd want to default that option to :data
though, right? Just in case someone's API spits out XML right now that's consumed by someone else who expects a top-level wrapper of <data></data>
.
Oh yes my mistake, you got it :)
This config file is the /.graphiticfg.yml
file, correct?
The one with the namespace:
setting? (That is the only setting I've ever seen mentioned for that file BTW.)
And that file always seems to be shown as beginning with 3 hyphens. Is that right?
Ours looks like this right now since we're not versioning our API:
---
namespace: /
Are those hyphens really necessary? I've always been curious about that...
That's really only used for generators, check out https://github.com/graphiti-api/graphiti/blob/master/lib/graphiti/configuration.rb
To be clear, the namespace:
setting is really only used for generators (which is what I'd gathered from the docs) but the config file itself is potentially used for other configuration, including this new xml_root:
configuration if it comes to pass?
Edit: I see looking at the code that it does appear that namespace:
is currently the only configuration setting possible, and it dictates where the schema.json
is expected to be...
Edit 2: But then there is mention of a Graphiti.config.schema_path
setting in a raised exception message. So, I guess I've taken us way far off topic. Excuse me for that. I'll stop pulling at threads now. π