Friday, July 31, 2026

JSON Custom Deserialize/Serialize Example

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 cenvert. 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 explicitely 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 {

    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 {

    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.

Friday, July 3, 2026

Go Unit Testing Example

As a software developer nowadays, we are expected to pick things up rather quickly. You can be assigned to a JIRA ticket to make changes to an app written in a programming language you are not that familiar with. It's normal to compare or ask for example how is it done in Go when you are used to the Java way. What do you do? Pass up on that task or jump straight in? You can refuse the task and pick something else but I wonder how management would see that.

Anyway, here's a look at unit testing in Go with a Java background. This Go repo, go-unit-testing is a port of this Java repo, tdd-junit.

Tools

  • Visual Studio Code 1.126 with a Go extension
  • Windows 11 Home 10.0.26200
  • Go version 1.22.1

Those are the tools I used to build this example.

Let's Go Unit Testing

Go has a built-in package for testing and surprisingly, it's called testing. It is part of Go's standard library. Comparing to Java, JUnit is not part of the standard library.

Below is the function that we will test. It's under the mathfun package.


package mathfun

func GetGcf(x int, y int) int {
	if y == 0 {
		return x
	} else {
		return GetGcf(y, x%y)
	}
}

Let's write the code that will test our function. Under the mathfun package, create the file math_fun_test.go. The _test.go tells Go that this file contains test functions. Below is the code of the said file:


package mathfun

import (
	"testing"
)

func TestWithTwoPositiveNumbers(t *testing.T) {
	actual := GetGcf(12, 16)
	expected := 4

	if expected != actual {
		t.Errorf(`GCF of 12 and 16, expected = %d, actual = %d`, expected, actual)
	}
}

func TestWithZero(t *testing.T) {
	actual := GetGcf(0, 6)
	expected := 6

	if expected != actual {
		t.Errorf(`GCF of 0 and 6, expected = %d, actual = %d`, expected, actual)
	}
}

Basically, the above code exercises the GetGcf function. Making sure to try and run every line of code in it. It checks for correct return values.

Ok, unlike Java, it's function names and not annotations (e.g. @Test) but still the function naming is DAMP. Test function names have the form of TestName, where Name is the DAMP (Descriptive and Meaning Phrase) of the test. The testing.T type parameter is used for logging and reporting your test result.

One thing that's been ingrained in me is the assertEquals. So it was surprising that Go didn't have an assert function but just do an if statement and based on that error out. I kinda like assert equals expected actual thing.

Running Go Unit Tests

The "go test" command. Executes all test function (names beginning with Test) it could find inside files ending in _test.go.


C:\workspace\go-unit-test\mathfun> go test
PASS
ok      go-unit-test/mathfun    0.578s

go text -v for verbose output
C:\workspace\go-unit-test\mathfun> go test -v
=== RUN   TestWithTwoPositiveNumbers
--- PASS: TestWithTwoPositiveNumbers (0.00s)
=== RUN   TestWithZero
--- PASS: TestWithZero (0.00s)
PASS
ok      go-unit-test/mathfun    0.556s

The command below runs the test in current directory. The last bit will run tests in all directories.


C:\workspace\go-unit-test\mathfun> go test .
ok      go-unit-test/mathfun    0.550s
C:\workspace\go-unit-test\mathfun> cd ..
C:\workspace\go-unit-test> go test .
?       go-unit-test    [no test files]
C:\workspace\go-unit-test> go test ./...
?       go-unit-test    [no test files]
ok      go-unit-test/mathfun    (cached)

As you can see above, Go caches successful test results to avoid running it again. The rule is, if the test binary is the same and there are no flags in the command line telling Go to rerun the test, then it will pull the cached test. Changes to your code or test code invalidates the cached test result so this isn't much of a big deal but if you really want to invalidate the cached test results, you can go clean -testcache

The command below does per package testing and the last two run the test per function.


C:\workspace\go-unit-test> go test ./mathfun
ok      go-unit-test/mathfun    (cached)
C:\workspace\go-unit-test> go test -run ^TestWithTwoPositiveNumbers$ go-unit-test/mathfun
ok      go-unit-test/mathfun    0.566s
C:\workspace\go-unit-test> go test -v -run ^TestWithTwoPositiveNumbers$ go-unit-test/mathfun
=== RUN   TestWithTwoPositiveNumbers
--- PASS: TestWithTwoPositiveNumbers (0.00s)
PASS
ok      go-unit-test/mathfun    0.527s

If you need more information on the go test command, it is right at your finger tips.


C:\workspace\go-unit-test\mathfun> go help test
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]

'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:

...snipped...

Go Unit Testing Summary

At the end of the day. Comparing it to JUnit, I'd say same nail pounded by a different hammer. Same principles really. As much as possible, all code has to be exercised and make sure the actual output is the same as the expected output.

Go test yourself