Efficient Byte Data Formatting with Custom Jackson Annotations
Understanding Byte Data Units
A byte (abbreviated as B) is the fundamental unit of digital information storage, consisting of 8 bits. In ASCII encoding, a single byte can represent standard English characters, numbers, punctuation, and control characters. Modern systems commonly use larger units for practical measurement:
While databases typically store raw byte values for precision, user interfaces require human-readable formats. This creates a common need for conversion between storage and display formats.
Implementation Scenario
Consider a cloud storage system where user data usage is stored in bytes but must be displayed in appropriate units. Two API endpoints demonstrate this requirement:
// Retrieves user storage usage
GET /api/user/storage/123
Response:
{
"userId": 123,
"usage": "2.3 GB"
}
// Updates user storage limit
POST /api/user/storage
{
"userId": 123,
"limit": "5.0 TB"
}
Custom Annotation Solution
Using Jackson's extensible serialization framework, we create a custom annotation to handle byte conversions automatically:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = ByteSerializer.class)
@JsonDeserialize(using = ByteDeserializer.class)
public @interface ByteFormat {
int precision() default 2;
}
Serialization Implementation
The serializer converts numeric byte values to formatted strings with appropriate units:
public class ByteSerializer extends JsonSerializer<Number> {
private int precision;
public ByteSerializer(int precision) {
this.precision = precision;
}
@Override
public void serialize(Number value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
if (value == null) return;
String formatted = ByteConverter.format(value.longValue(), precision);
gen.writeString(formatted);
}
}
Deserialization Implementation
The deserializer converts formatted strings back to raw byte values:
public class ByteDeserializer extends JsonDeserializer<Number> {
@Override
public Number deserialize(JsonParser parser,
DeserializationContext context) throws IOException {
String text = parser.getText();
return ByteConverter.parse(text);
}
}
Conversion Utility
The core conversion logic handles both formatting and parsing:
public class ByteConverter {
private static final long KB = 1024L;
private static final long MB = KB * 1024;
private static final long GB = MB * 1024;
private static final long TB = GB * 1024;
public static String format(long bytes, int decimalPlaces) {
if (bytes >= TB) {
return String.format("%." + decimalPlaces + "f TB", bytes / (double) TB);
}
if (bytes >= GB) {
return String.format("%." + decimalPlaces + "f GB", bytes / (double) GB);
}
if (bytes >= MB) {
return String.format("%." + decimalPlaces + "f MB", bytes / (double) MB);
}
if (bytes >= KB) {
return String.format("%." + decimalPlaces + "f KB", bytes / (double) KB);
}
return bytes + " B";
}
public static long parse(String formatted) {
String clean = formatted.trim().toUpperCase();
if (clean.endsWith("TB")) {
double value = Double.parseDouble(clean.replace("TB", ""));
return (long) (value * TB);
}
if (clean.endsWith("GB")) {
double value = Double.parseDouble(clean.replace("GB", ""));
return (long) (value * GB);
}
if (clean.endsWith("MB")) {
double value = Double.parseDouble(clean.replace("MB", ""));
return (long) (value * MB);
}
if (clean.endsWith("KB")) {
double value = Double.parseDouble(clean.replace("KB", ""));
return (long) (value * KB);
}
if (clean.endsWith("B")) {
return Long.parseLong(clean.replace("B", ""));
}
return Long.parseLong(clean);
}
}
Usage Example
Apply the annotation to DTO fields for automatic conversion:
public class StorageInfo {
private Long userId;
@ByteFormat(precision = 1)
private Long currentUsage;
@ByteFormat(precision = 2)
private Long storageLimit;
}