Search by

brocode / module-webapi-yaml

brosenberger

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!

Buymeacoffee

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-08-24 12:53 UTC

This package is auto-updated.

Last update: 2026-08-24 13:16:31 UTC


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

  • GET any REST endpoint with Accept: application/yaml and get a YAML response instead of JSON.
  • POST/PUT any endpoint with Content-Type: application/yaml and 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 one etc/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: asserts etc/di.xml still 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, DataObject unwrapping.
  • Deserializer/YamlTest: mapping decode, native-type decode, no root-element unwrap needed, malformed-input and non-string-input error paths.

License

MIT