Do you need to support multiple environments? Tired of spinning up instances? Perhaps custom JSON serialization and deserialization can help.
The Ask
Here’s the situation. You got a web API and then one day your customer wants to roll out two more development environments. This will result in three instances hitting your web API. Production and preproduction environments remain a one to one relationship. As it stands, your IDs are integers in all environments. Constraint number one, you can’t afford to spin up more development environments to match your customer’s new development instances due to limited staff. Second, can’t afford to spin more infrastructure. Third, you don’t want to maintain more pods/servers. Lastly, in the development environment, you need to know which development environment the ID came from.
The Solution
The solution is custom JSON serialization and deserialization of course. LOL, this is what the blog is all about. Anyway, we need the web API to be able to accept a number and a string for incoming requests. This way, when deployed to higher environments (e.g. production and preproduction), incoming requests with IDs that are numbers will not break things.
Now, when passing the ID down the line (e.g. internal systems), we need to make sure a number and string can be passed as either of both types to make sure nothing breaks. Clear so far? How’s your imagination doing? Can you imagine it now? Got your head wrapped around it?
As for persistence, that’s another story but ALTER TABLE should do the trick. Also, this blog is about custom JSON serialization and deserialization. The database operations are another blog in itself. So not talking about that.
Custom JSON Deserialization
Below is the test to make sure customerId is deserialized as strings.
//... imports snipped...
@SpringBootTest
public class RequestDtoTest {
@Test
public void customerIdNumberToString() {
String jsonInput = "{\"customerId\":12345}";
RequestDto dto = new ObjectMapper().readValue(jsonInput, RequestDto.class);
assertNotNull(dto.getCustomerId());
assertEquals("12345", dto.getCustomerId());
}
@Test
public void customerIdMaintainedAsString() {
String jsonInput = "{\"customerId\":\"dev1-12345\"}";
RequestDto dto = new ObjectMapper().readValue(jsonInput, RequestDto.class);
assertNotNull(dto.getCustomerId());
assertEquals("dev1-12345", dto.getCustomerId());
}
}Clear enough? Just imagine the web API received a request. As you can see it is able to accept a number (e.g. 12345 non development environments) and a string (e.g. dev1-12345 for the development environments). Below are the code that handles the custom JSON deserialization. First is to create your custom JSON deserializer of course. Pretty straight forward wouldn’t you say? If the token is a string, no need to convert. Just return it right away. If it is a number, convert to string. Anything else, return as string.
Here’s a tip. Actually, we didn’t need to explicitly do this. Why you ask?, Because Jackson coerces values in strings. It’s worth advocating this explicit approach. Makes it clearer to the future developers as to what’s going on. Also limit the type to only integers can be coerced.
//... imports snipped...
public class NumberToStringDeserializer extends StdDeserializer<String> {
public NumberToStringDeserializer() {
super(String.class);
}
@Override
public String deserialize(JsonParser parser, DeserializationContext context) {
JsonToken token = parser.currentToken();
if (token == JsonToken.VALUE_NULL) {
return null;
}
if (token == JsonToken.VALUE_STRING) {
return parser.getString();
}
if (token == JsonToken.VALUE_NUMBER_INT) {
return parser.getNumberValue().toString();
}
// anything else
return (String) context.handleUnexpectedToken(String.class, parser);
}
}Finally, is to declare the deserializer (@JsonDeserialize(using = NumberToStringDeserializer.class) in your POJO. Well done. You have got yourself a custom JSON deserializer.
//... imports snipped...
public class RequestDto {
@JsonDeserialize(using = NumberToStringDeserializer.class)
@JsonProperty("customerId")
private String customerId;
public String getCustomerId() {
return customerId;
}
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
@Override
public String toString() {
return "RequestDto{" +
"customerId='" + customerId + '\'' +
'}';
}
}Custom JSON Serialization
Below is the test to make sure transactionId is serialized as a string (i.e. for dev environments) if it can’t be converted into a number (i.e. for prod and preprod environments).
//... imports snipped...
@SpringBootTest
public class ResponseDtoTest {
@Test
public void transactionIdNumberToString() {
ResponseDto dto = new ResponseDto();
dto.setTransactionId("54321");
String expectedJson = "{\"transactionId\":54321}";
String actualJson = new ObjectMapper().writeValueAsString(dto);
assertEquals(expectedJson, actualJson);
}
@Test
public void transactionIdMaintainedAsString() {
ResponseDto dto = new ResponseDto();
dto.setTransactionId("dev2-54321");
String expectedJson = "{\"transactionId\":\"dev2-54321\"}";
String actualJson = new ObjectMapper().writeValueAsString(dto);
assertEquals(expectedJson, actualJson);
}
}Right, so if we can convert the value into a number, we do it. Otherwise, send it over the wire as a string.
//... imports snipped...
public class StringToNumberSerializer extends StdSerializer<String> {
public StringToNumberSerializer() {
super(String.class);
}
@Override
public void serialize(String value, JsonGenerator generator, SerializationContext context) throws JacksonException {
try {
generator.writeNumber(Integer.parseInt(value));
} catch (NumberFormatException e) {
generator.writeString(value);
}
}
}Lastly, declare the serializer (@JsonSerialize(using = StringToNumberSerializer.class) in your POJO. Amazing! You’ve got a custom JSON serializer.
//... imports snipped...
public class ResponseDto {
@JsonSerialize(using = StringToNumberSerializer.class)
private String transactionId;
public String getTransactionId() {
return transactionId;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
@Override
public String toString() {
return "ResponseDto{" +
"transactionId='" + transactionId + '\'' +
'}';
}
}JSON Custom Deserialize/Serialize Conclusion
I’m using IntelliJ IDEA 2023.3.4 (Community Edition). You should be able to run the unit tests and you should have something like below.

There you have it. A straight forward custom JSON deserialization and serialization. Making you save money and use less resources. You can grab the repo from GitHub.
Custom JSON bourne serialization and deserialization just for you.