A Problem Worth Writing About
Every modernization engagement brings its own set of surprises, but once in a while you encounter an issue that is both technically intriguing and representative of the realities of working across distributed and mainframe environments.
Which is all to say, I have found myself reflecting on a particularly interesting problem we solved over the past couple of weeks. We were assisting a prospect evaluating CloudFrame for converting part of a larger CICS transaction into a standalone Java API. The converted service ran perfectly on Linux. Predictable, fast, and structurally identical to the legacy business logic. The next phase was to deploy the same API on z/OS UNIX System Services. Normally such deployments sit behind Liberty and get invoked via CICS, but in this case the client wanted to have the Java API directly on z/OS without involving Liberty at all.
We bundled the application, deployed it on z/OS, and expected a straightforward run. And then, as it often happens in modernization projects, the “straightforward” part evaporated.
The Symptom: Valid JSON, Invalid Behavior
The API endpoint was reachable and functional. Linux clients could POST JSON payloads without any issues. The same payload, sent from a Java client running on z/OS, however, consistently failed with exceptions coming from Jackson's deserialization logic.
And this time the error wasn’t even reaching our controller. There was no controller log, no validation error, and no application-level exception. The request was being rejected long before it touched our code.
The actual error looked like this:
This was the first major hint. The JSON itself was perfectly valid. The structure was correct. The payload worked everywhere else. Yet Jackson was seeing the very first byte as 'B', which made no sense unless the raw encoding itself was wrong.
Something was fundamentally different about the bytes arriving from a z/OS-originated request. We started suspecting codepage, but weren't sure where it is
Investigating the Request Stream
The first step was to inspect the raw bytes arriving at the server before Spring or Jackson touched them.
Requests originating from Linux or Windows contained the expected UTF-8 encoded ASCII characters:
Requests originating from z/OS told an entirely different story:
It was at this moment that the root cause became immediately clear:
The payload was not UTF-8. It was EBCDIC.
A Java application running on z/OS uses the system’s default charset unless explicitly overridden. So when the z/OS client executed:
outputStream.write(jsonString.getBytes());
it produced EBCDIC bytes, not UTF-8 bytes.
To Jackson, and to every JSON parser in the world, this looked like corrupted, illegal input.
Understanding the Point of Failure
Jackson expects UTF-8 JSON. z/OS was sending EBCDIC JSON.
The sequence looked like this:
There was nothing wrong with the data. There was nothing wrong with the controller. There was nothing wrong with Jackson. The issue was simply that the first byte of the JSON “{” in EBCDIC does not map to “{” in UTF-8.
Designing a Platform-Aware Fix
We needed a solution that:
- Detects whether inbound JSON is UTF-8 or EBCDIC
- Converts EBCDIC → UTF-8 before Jackson sees it
- Ensures outbound responses are encoded appropriately
- Works in a multithreaded environment
- Requires zero changes from customers
The last point is extremely important in modernization scenarios. The goal is not just “make it work,” but make it work without asking customers to change how they operate. To meet all these requirements, we implemented a Spring ControllerAdvice interceptor.
The Architecture: Auto-Detect → Convert → Track
Here is the overall flow we implemented:
The inbound encoding decision is stored in a ThreadLocal<Boolean>, ensuring correct behavior even under heavy concurrency.
The Core Implementation
Key aspects:
- ASCII-dominance detection is reliable for JSON
- Outbound uses ObjectMapper.writeValueAsBytes to avoid toString() pitfalls
- ThreadLocal ensures per-request isolation
Why This Approach Works
The real strength of the solution is not the conversion itself. It is the auto-detection and context propagation. Encoding mismatches are one of the most common hidden traps when integrating distributed and mainframe-native workloads. A solution that “just converts” is not enough. It has to convert only when necessary and behave transparently when not.
By making the interceptor fully automatic, with no required changes from callers, we delivered a solution that is:
- robust
- platform-aware
- easy for customers
- safe for mixed environments
This is precisely the level of engineering maturity required for real-world modernization.
Closing Thoughts
This issue is a reminder that modernization is rarely just a matter of translating COBOL into Java. It is an exercise in understanding decades of platform expectations, encoding defaults, and runtime assumptions. Something as simple as a JSON payload becomes an engineering challenge when one side speaks UTF-8 and the other speaks EBCDIC.
At CloudFrame, these are the kinds of challenges we enjoy solving. They deepen our understanding of the mainframe ecosystem and reinforce why thoughtful, platform-aware design is critical in building modernization solutions that truly work for enterprises.
A good way to end the month, with one more problem solved, and one more lesson gained. If you’d like the full implementation or want to go deeper into this topic, reach out to us in the comments. We'd love to hear from you!