I was building a Rails API endpoint for a third-party education technology service to integrate with, and my test suite was completely green. Every scenario I could think of worked perfectly: valid payloads parsed correctly, invalid ones returned proper error messages, edge cases were handled. Then the third-party team started their integration work, and things immediately broke in a way that made no sense. Some attributes from their request payload were showing up in my Rails params hash, but others were just missing. Not null, not empty—completely absent, as if they’d never been sent.
The debugging process was frustrating because the problem was intermittent in a weird way. Some fields would arrive, others wouldn’t, and there was no obvious pattern. It wasn’t about field names, data types, or nesting depth. I logged the raw request body and could see that all the data was there in the HTTP request, properly formatted as JSON. But by the time Rails finished parsing the request and handed me the params, half of it had vanished. The gap between “data definitely arrived at the server” and “data definitely isn’t in params” is usually where Rails’ middleware lives, so I started looking at how Rails parses request bodies.
That’s when I noticed the Content-Type header they were sending: application/vnd.ims.lis.v1.score+json. Rails is smart about content types—it knows that application/json means JSON and should be parsed accordingly, that application/xml means XML, and so on. But this custom MIME type wasn’t in Rails’ registry, so Rails did what it does with unknown content types: treated the body as plain text and stuffed the whole thing into a single string parameter instead of parsing it as structured data. From Rails’ perspective, it was behaving correctly—it received data in an unknown format and preserved it without making assumptions. From my perspective, it looked like the data was disappearing because I was expecting a parsed hash.
The fix is straightforward once you know what’s happening. Rails lets you register custom MIME types as synonyms for existing ones in config/initializers/mime_types.rb:
json_mime_type_synonyms = %w[application/vnd.ims.lis.v1.score+json]
Mime::Type.register("application/json", :json, json_mime_type_synonyms)
This tells Rails that when it sees application/vnd.ims.lis.v1.score+json in a Content-Type header, it should treat it exactly like application/json and parse the body accordingly. The data structure was JSON all along, just labeled with a vendor-specific MIME type that follows a convention where the +json suffix indicates the underlying format. Rails doesn’t automatically parse these vendor MIME types even though the suffix makes the format clear, which is probably the right default behavior since not every +json suffix necessarily means the content should be parsed the same way.
What this experience reminded me is that third-party integrations always surface assumptions you didn’t realize you were making. When I wrote my tests, I implicitly assumed that requests would come in with application/json as the content type because that’s what every tool I use for API testing sends by default. Curl sends it, Postman sends it, RSpec request specs send it. But real-world integrations don’t follow your assumptions—they follow standards and conventions that might be slightly different from yours. The education technology vendor was following the IMS Global Learning Consortium standards, which define specific MIME types for different payload types. They were being more correct than my generic JSON approach, and my API broke because I hadn’t accounted for that correctness.
The broader pattern here is that framework conventions work great until they don’t, and the moment they don’t is usually when you’re integrating with someone else’s system. Rails makes reasonable assumptions about what content types mean, but those assumptions are based on common usage, not comprehensive coverage of every possible MIME type. If you’re building APIs that need to work with external systems—especially in domains that have their own standards like education, healthcare, or finance—you need to explicitly handle the MIME types those systems use. It’s a small configuration change, but it’s the difference between an integration that works and one that mysteriously drops half the data.