Content Negotiation in ASP.NET Core: JSON vs XML vs MessagePack vs Custom Formatters β Enterprise Decision Guide

Content negotiation in ASP.NET Core is one of those features that most teams leave on default until a client integration forces the conversation. The framework defaults to JSON β and for the vast majority of APIs, that is exactly the right call. But when you are designing APIs that serve heterogeneous clients, internal microservices with performance constraints, or legacy integrations that expect XML, the decisions you make around content negotiation will directly affect interoperability, payload size, and serialisation overhead across your entire platform.
π Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. π https://www.patreon.com/CodingDroplets
How Does Content Negotiation Work in ASP.NET Core?
Content negotiation is the HTTP mechanism by which a client declares the media types it can consume via the Accept header, and the server selects the best matching output formatter to serialise the response. ASP.NET Core implements this through the ObjectResult pipeline β when a controller action returns an object and the framework wraps it in an ObjectResult, the DefaultOutputFormatterSelector inspects the Accept header and picks the first registered formatter whose supported media types match.
If no Accept header is present, or if none of the registered formatters match, the framework falls back to the first registered formatter β which by default is System.Text.Json. When RespectBrowserAcceptHeader is false (the default), browser requests that send Accept: text/html or wildcard headers are still served JSON, which is usually desirable for API projects.
The key architectural insight: output formatters are evaluated in registration order. The first match wins. This means formatter registration sequence is a deliberate design decision, not a configuration afterthought.
When Should You Enable Additional Formatters?
The default JSON-only configuration is correct for most greenfield APIs. The question is: what signals indicate that you actually need to expand it?
Expand beyond JSON when:
- A consuming client explicitly requires XML β enterprise Java systems, legacy SOAP-adjacent integrations, or financial data feeds
- You are building internal microservices where MessagePack's binary format would yield meaningful payload savings at volume
- Your API must support an industry-specific media type such as
application/vnd.api+json,application/hal+json, orapplication/ld+json - A partner's SDK is hardcoded to a specific
Acceptheader and you cannot modify the client - You need
application/csvor similar purpose-built formats for bulk data export endpoints
Stay JSON-only when:
- All consumers are browser clients or JavaScript-based frontends
- You control both the API and all its clients
- Your team has no operational familiarity with the trade-offs of additional formatters
- You are building a public API that should have the simplest possible integration story
- Response size is not a bottleneck β the overhead of JSON rarely matters at typical API scales
The Four Formatter Strategies
JSON (System.Text.Json)
The default. Since .NET 5, System.Text.Json is the built-in serialiser. It is fast, allocation-friendly, and deeply integrated with the ASP.NET Core pipeline. For most teams, this is where content negotiation decisions start and end.
The relevant enterprise consideration here is configuration: ensure source generation is enabled for hot-path controllers, configure JsonSerializerOptions as a singleton to avoid repeated reflection, and align PropertyNamingPolicy with your API's contract guarantees. For a deep comparison of serialisation libraries, see System.Text.Json vs Newtonsoft.Json in .NET: Which Should Your Enterprise Team Use in 2026? on Coding Droplets.
XML (XmlSerializer / DataContractSerializer)
XML support ships in the Microsoft.AspNetCore.Mvc.Formatters.Xml package. You add it with AddXmlSerializerFormatters() or AddXmlDataContractSerializerFormatters(). The distinction matters: XmlSerializer honours [XmlElement] attributes and is more interoperable with external tooling; DataContractSerializer honours [DataContract] / [DataMember] and is tighter but less portable.
Trade-off summary: XML responses are 2β4x larger than their JSON equivalents for typical domain objects. Serialisation is meaningfully slower. For internal services, this is rarely worth it. For B2B integration scenarios where the partner mandates XML, you have no choice β but consider generating XML from a dedicated serialisation layer rather than enabling it globally across all endpoints. Use [Produces("application/json")] or [ApiController]'s negotiation attributes to constrain format at the action or controller level wherever XML is not appropriate.
MessagePack
MessagePack is a binary serialisation format β typically 20β40% smaller than JSON and faster to serialise and deserialise. The MessagePack-CSharp library by Neuecc is the standard integration for .NET and provides an ASP.NET Core formatter package (MessagePackMediaTypeFormatter).
The enterprise trade-off is human-readability versus efficiency. MessagePack responses are opaque to plain HTTP clients and tools like Postman without a MessagePack plugin. Debugging integration issues becomes harder. This cost is acceptable for internal gRPC-adjacent service buses or high-throughput event feeds where you control both ends. It is rarely worth it for external-facing APIs where developer experience matters. A meaningful alternative worth evaluating in this space is gRPC vs REST in .NET Microservices β if you need binary efficiency between services, Protobuf over gRPC may be a cleaner architectural fit.
Custom Formatters
ASP.NET Core's formatter pipeline is extensible. You can implement TextOutputFormatter or OutputFormatter to handle any media type: text/csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for Excel, application/pdf, or custom vendor MIME types.
Custom formatters follow a simple contract: declare supported media types in the constructor and override WriteResponseBodyAsync. They are registered by inserting into MvcOptions.OutputFormatters at the appropriate position β insert at index 0 only if you want them to be the default fallback.
When custom formatters pay off: bulk export endpoints where the response shape is fixed, API products that must conform to a hypermedia media type, or any scenario where you need the HTTP negotiation mechanism to drive format selection transparently across multiple controller actions.
Decision Matrix for Enterprise Teams
| Scenario | Recommended Formatter Strategy |
|---|---|
| Greenfield REST API, all clients JS/mobile | JSON only (System.Text.Json) |
| B2B integration, partner requires XML | JSON default + XML opt-in via [Produces] on specific endpoints |
| Internal microservices, high message volume | Evaluate MessagePack or gRPC β not both |
| Public API, mixed client ecosystem | JSON only, consider vendor MIME types for versioning |
| Bulk export (CSV, Excel) | Custom formatter scoped to export controller |
| Hypermedia API (HAL, JSON:API) | Custom formatter or a dedicated library per media type |
Should You Enable Content Negotiation Globally or Per-Endpoint?
This is the architectural question most guides skip. Global formatter registration means every controller action participates in negotiation, which creates surface area for unexpected behaviour: a client that accidentally sends Accept: application/xml will receive XML from an endpoint you never intended to serve XML from.
The production-safe pattern for mixed-format APIs is per-endpoint content negotiation:
- Register all formatters globally in
MvcOptions - Constrain individual endpoints with
[Produces("application/json")]or[ProducesResponseType]to limit which formats they participate in - Only leave the default
[ApiController]behaviour for endpoints that genuinely should respect all registered formatters
This gives you the flexibility of the global registration without the footgun of accidental format leakage. The ASP.NET Core documentation on formatting response data covers the RespectBrowserAcceptHeader and ReturnHttpNotAcceptable settings that you should review before going to production with a multi-formatter setup.
Anti-Patterns to Avoid
Registering formatters without ReturnHttpNotAcceptable = true: If a client sends an Accept header for a media type you do not support, the default behaviour is to fall back to the first registered formatter and serve JSON anyway, with a 200 OK. This silently ignores the client's negotiation signal. Set ReturnHttpNotAcceptable = true in MvcOptions and return 406 Not Acceptable explicitly.
Treating formatter order as arbitrary: The first matching formatter wins. If you insert the XML formatter at index 0, every unadorned request will be evaluated for XML first. This is a slow path for JSON-only consumers and an operational surprise waiting to happen.
Enabling XML globally in a microservices fleet: The cost compounds. XML processing overhead is small per-request but accumulates across service mesh hops. Profile before enabling.
Custom formatters with no content-type validation on input: Input formatters (for Content-Type negotiation on request bodies) need the same disciplined registration. A custom formatter that accepts */* as a supported input type will compete with the JSON formatter for every POST body.
Forgetting [ApiController] inference rules: [ApiController] changes how Accept headers interact with action signatures. Review which return types participate in negotiation and which short-circuit it with IActionResult or ActionResult<T>. For a fuller treatment of return type design, see IActionResult vs TypedResults vs Results in ASP.NET Core: Enterprise API Response Design Decision Guide on Coding Droplets.
The Enterprise Recommendation
For most enterprise teams, the right answer is: use JSON exclusively, configure it correctly, and document which endpoints deviate and why. Content negotiation is a feature that pays dividends when you need it and adds invisible complexity when you do not.
If you do expand the formatter set, treat it as an API contract decision β not a configuration decision. Add it to your API governance checklist, enforce format constraints at the endpoint level with [Produces], and make the permitted media types explicit in your OpenAPI specification so consumers know what to expect.
β Prefer a one-time tip? Buy us a coffee β every bit helps keep the content coming!
Frequently Asked Questions
Does enabling the XML formatter affect JSON serialisation performance?
No. Formatters only execute when selected by the negotiation algorithm. If a request has no Accept header or sends Accept: application/json, the XML formatter is never invoked and incurs no overhead.
What happens if a client sends Accept: application/xml but XML is not registered?
With default settings, ASP.NET Core will fall back to the JSON formatter and return JSON with a 200 OK. This silently ignores the client preference. Set ReturnHttpNotAcceptable = true to return a 406 Not Acceptable response instead.
Is MessagePack suitable for public APIs? Generally not. The binary format is opaque to browser clients and generic HTTP tools without plugins. MessagePack is best suited for internal service-to-service communication where you control both endpoints and have validated the latency and payload savings justify the debugging overhead.
Can I use content negotiation with Minimal APIs?
Yes, but the mechanism is different. Minimal APIs do not use ObjectResult by default. You need to explicitly configure output formatters for Minimal API endpoints, or use Results.Ok(value) and ensure the endpoint's response type participates in the formatter pipeline via route handler configuration.
How do I prevent a specific endpoint from serving XML even when XML is globally registered?
Apply [Produces("application/json")] to the controller class or specific action method. This constrains the list of eligible formatters to JSON only for that endpoint, regardless of the global formatter registration.
Should the XML formatter come before or after JSON in the registration order?
JSON should remain first unless XML must be the default format for clients with no Accept header. In enterprise APIs, JSON almost always stays first β XML is opt-in via explicit Accept: application/xml headers from clients that need it.
What is the difference between AddXmlSerializerFormatters and AddXmlDataContractSerializerFormatters?
AddXmlSerializerFormatters uses XmlSerializer under the hood, which respects [XmlElement], [XmlAttribute], and related attributes β better for cross-platform interoperability. AddXmlDataContractSerializerFormatters uses DataContractSerializer, which respects [DataContract] and [DataMember] β more aligned with WCF-style contracts. For greenfield integrations, prefer XmlSerializer.





