brocode / module-webapi-yaml
Registers a YAML renderer and deserializer for the Magento 2 REST API alongside the built-in JSON and XML formats, via content negotiation (Accept / Content-Type) - no core edits.
Package info
github.com/brosenberger/module-webapi-yaml
Type:magento2-module
pkg:composer/brocode/module-webapi-yaml
Fund package maintenance!
Requires
- php: ~8.1.0||~8.2.0||~8.3.0||~8.4.0
- magento/framework: >=103.0.0
- symfony/yaml: ^6.4||^7.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Registers a YAML renderer and deserializer for the Magento 2 REST API alongside
the built-in JSON and XML formats, via the same content-negotiation mechanism
core already uses (Accept / Content-Type headers) — no core edits, no
override.
composer require brocode/module-webapi-yaml bin/magento module:enable BroCode_WebapiYaml bin/magento cache:flush bin/magento setup:di:compile
Built as the reference implementation for a companion article on brocode.at about Magento's REST content-negotiation mechanism in general — this module is the "and here's how you'd add your own format" half of that article.
What this is
GETany REST endpoint withAccept: application/yamland get a YAML response instead of JSON.POST/PUTany endpoint withContent-Type: application/yamland a YAML body, and it decodes exactly like a JSON or XML body would.- Two small classes (
Model/Webapi/Rest/Response/Renderer/Yaml.php,Model/Webapi/Rest/Request/Deserializer/Yaml.php), each under 90 lines, wired via oneetc/di.xml.
The gotcha this module exists to document
Adding a response format is genuinely as simple as it sounds: implement
RendererInterface, add one <item> to RendererFactory's renders
argument. That argument's base declaration lives in a normal module di.xml
(vendor/magento/module-webapi/etc/di.xml), and Magento's DI array-merge
unions a module's additive <item> with core's existing ones cleanly.
Adding a request format is not the same shape of easy. DeserializerFactory's
deserializers argument is declared in the root app/etc/di.xml, not a
module di.xml — and that file does not participate in the same
merge-by-item-name behavior. Confirmed live, via reflection on the compiled
factory objects with this module enabled:
RendererFactory->_renders → 6 entries (default, json, xml×3, yaml) ✅ merged
DeserializerFactory->_deserializers → 1 entry (yaml only) ❌ replaced
A module that adds only its own application_yaml entry silently deletes
core's application_json, application_xml, text_xml, and
application_xhtml_xml entries from the runtime array. Every existing REST
consumer's POST/PUT with a JSON or XML body starts failing with:
{"message":"Server cannot understand Content-Type HTTP header media type application/json","trace":null}
— on every route, site-wide, the moment this module is enabled, until it also
redeclares those four entries. etc/di.xml in this repo does exactly that;
Test/Unit/Etc/DiConfigTest.php is a regression guard against someone
"simplifying" it back down to just the new entry.
The bulk API's XML quirk
Not this module's problem to solve (this module only registers YAML), but
worth documenting since it was verified alongside this module and is the
kind of thing nobody finds until it costs an afternoon: Magento's core XML
parser (Magento\Framework\Xml\Parser::_xmlToArray()) collapses repeated
sibling elements with the same tag name into one array key rather than a
positional list. For a normal single-entity request body that's irrelevant.
For a bulk request body (/async/bulk/V1/...), which needs N distinct
entities in one payload, naive <item>...</item><item>...</item> XML does
not parse into two items — it fails outright.
Live-verified against POST /async/bulk/V1/customers/isEmailAvailable on a
Magento 2.4.8-p5 instance:
# JSON baseline — 2 items accepted curl -X POST ".../rest/async/bulk/V1/customers/isEmailAvailable" \ -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \ --data-raw '[{"customerEmail":"a@example.com"},{"customerEmail":"b@example.com"}]' # → 202, request_items: [{id:0,status:accepted},{id:1,status:accepted}] # naive XML — fails, and the error doesn't point at the real problem curl -X POST ".../rest/async/bulk/V1/customers/isEmailAvailable" \ -H "Content-Type: application/xml" -H "Authorization: Bearer $TOKEN" \ --data-raw '<request><item><customerEmail>a@example.com</customerEmail></item><item><customerEmail>b@example.com</customerEmail></item></request>' # → 400, {"message":"\"%fieldName\" is required.","parameters":{"fieldName":"customerEmail"}} # XML with uniquely-named items — works curl -X POST ".../rest/async/bulk/V1/customers/isEmailAvailable" \ -H "Content-Type: application/xml" -H "Authorization: Bearer $TOKEN" \ --data-raw '<request><item_0><customerEmail>a@example.com</customerEmail></item_0><item_1><customerEmail>b@example.com</customerEmail></item_1></request>' # → 202, request_items: [{id:0,status:accepted},{id:0,status:accepted}]
Two things worth flagging about that last response: it works (both items are
genuinely accepted, confirmed 202 with errors: false), but the reported
id field is 0 for both items rather than 0/1 — unlike the JSON
baseline. If a caller correlates async results back to input items by that
id, the XML path doesn't give it the same guarantee JSON does. Not
independently root-caused in this pass; flagged here rather than glossed
over.
Runnable copies of all three request bodies above — re-verified live, not
just pasted from a terminal history — are in samples/, along
with the exact curl invocations.
Why this is a narrow capability, not a recommendation
Real-world demand for a custom REST wire format is almost entirely XML —
SAP/legacy-ERP integrations that already speak XML natively, which core
already covers. YAML earns its place here as a clean worked example (real,
typed, no character-restriction baggage compared to XML — see
Test/Unit/Model/Webapi/Rest/Response/Renderer/YamlTest.php's
code: 'NO'-vs-YAML-1.1-boolean-literal test for a concrete case where that
matters), not as a suggestion that Magento merchants need YAML support.
Verification
composer install && vendor/bin/phpunit — no Magento install needed. Covers:
DiConfigTest: assertsetc/di.xmlstill redeclares all four core deserializer entries alongside the new one (the regression this module exists to prevent), and that the renderer side stays additive-only.Renderer/YamlTest: native-type round-trip, the YAML-boolean-literal auto-quoting case,DataObjectunwrapping.Deserializer/YamlTest: mapping decode, native-type decode, no root-element unwrap needed, malformed-input and non-string-input error paths.
License
MIT