| Situation | Solution |
Format a BigDecimal as a plain number string without scientific notation | Custom serializer |
Deserialize a legacy API that sends booleans as "Y"/"N" | Custom deserializer |
| Serialize a third-party class you cannot annotate | Custom serializer + SimpleModule |
| Flatten or restructure nested JSON on read | Custom deserializer |
| Write a type discriminator field during serialization | Custom serializer |
| Simple field renaming / date formatting | Annotations (@JsonProperty, @JsonFormat) — no custom code needed |
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.math.BigDecimal;
/** Serialize BigDecimal as a plain string without scientific notation. */
public class MoneySerializer extends StdSerializer<BigDecimal> {
public MoneySerializer() {
super(BigDecimal.class);
}
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
if (value == null) {
gen.writeNull();
} else {
// toPlainString() avoids "1.23E+4" notation
gen.writeString(value.toPlainString());
}
}
}
// --- Registration ---
SimpleModule module = new SimpleModule("MoneyModule");
module.addSerializer(BigDecimal.class, new MoneySerializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
// Test
record Invoice(String id, BigDecimal amount) {}
String json = mapper.writeValueAsString(new Invoice("INV-1", new BigDecimal("1234567.89")));
// {"id":"INV-1","amount":"1234567.89"}
Always extend StdSerializer<T> (not the raw JsonSerializer<T>) — it provides useful helper methods and ensures correct type token handling.
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;
/** Deserialize "Y"/"N" strings to Boolean. */
public class YesNoDeserializer extends StdDeserializer<Boolean> {
public YesNoDeserializer() {
super(Boolean.class);
}
@Override
public Boolean deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String value = p.getText();
return switch (value.toUpperCase()) {
case "Y", "YES", "TRUE", "1" -> Boolean.TRUE;
case "N", "NO", "FALSE", "0" -> Boolean.FALSE;
default -> throw new JsonParseException(p,
"Cannot parse boolean from: " + value);
};
}
}
// Registration
SimpleModule module = new SimpleModule();
module.addDeserializer(Boolean.class, new YesNoDeserializer());
module.addDeserializer(boolean.class, new YesNoDeserializer());
ObjectMapper mapper = new ObjectMapper().registerModule(module);
// Test
record LegacyFlag(String name, Boolean active) {}
LegacyFlag flag = mapper.readValue(
"{\"name\":\"feature-x\",\"active\":\"Y\"}", LegacyFlag.class);
System.out.println(flag.active()); // true
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;
record Money(long cents, String currency) {}
/** Serialize Money as "12.34 USD" */
class MoneyToStringSerializer extends StdSerializer<Money> {
MoneyToStringSerializer() { super(Money.class); }
@Override
public void serialize(Money v, JsonGenerator gen, SerializerProvider p)
throws IOException {
gen.writeString("%.2f %s".formatted(v.cents() / 100.0, v.currency()));
}
}
/** Deserialize "12.34 USD" back to Money */
class StringToMoneyDeserializer extends StdDeserializer<Money> {
StringToMoneyDeserializer() { super(Money.class); }
@Override
public Money deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String text = p.getText().trim();
String[] parts = text.split(" ", 2);
long cents = Math.round(Double.parseDouble(parts[0]) * 100);
return new Money(cents, parts.length > 1 ? parts[1] : "USD");
}
}
// Register both
SimpleModule module = new SimpleModule("MoneyModule")
.addSerializer(Money.class, new MoneyToStringSerializer())
.addDeserializer(Money.class, new StringToMoneyDeserializer());
ObjectMapper mapper = new ObjectMapper().registerModule(module);
String json = mapper.writeValueAsString(new Money(1234, "EUR")); // "12.34 EUR"
Money back = mapper.readValue("\"12.34 EUR\"", Money.class);
| Method | Writes |
writeStartObject() / writeEndObject() | { / } |
writeStartArray() / writeEndArray() | [ / ] |
writeFieldName(name) | "name": |
writeStringField(name, value) | "name":"value" shortcut |
writeNumberField(name, value) | "name":42 shortcut |
writeBooleanField(name, value) | "name":true shortcut |
writeNullField(name) | "name":null |
writeString(value) | String value token |
writeNumber(value) | Number value token |
writeNull() | null token |
writeObject(obj) | Delegates to registered serializer for obj |
A ContextualDeserializer inspects annotations on the property being deserialized and returns a configured instance — useful when behaviour varies per field.
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.core.JsonParser;
import java.lang.annotation.*;
import java.io.IOException;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface TrimString {} // custom annotation
public class TrimmingStringDeserializer extends StdDeserializer<String>
implements ContextualDeserializer {
private final boolean trim;
public TrimmingStringDeserializer() { this(false); }
private TrimmingStringDeserializer(boolean trim) {
super(String.class);
this.trim = trim;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctx,
BeanProperty property) {
boolean shouldTrim = property != null
&& property.getAnnotation(TrimString.class) != null;
return new TrimmingStringDeserializer(shouldTrim);
}
@Override
public String deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String value = p.getText();
return trim ? value.trim() : value;
}
}
// Usage on a field
class UserForm {
@TrimString
@JsonDeserialize(using = TrimmingStringDeserializer.class)
public String username;
}
Instead of registering globally via a module, apply a serializer or deserializer to a specific field with annotations.
import com.fasterxml.jackson.databind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDate;
public class Order {
public String id;
@JsonSerialize(using = MoneySerializer.class)
@JsonDeserialize(using = MoneyDeserializer.class)
public BigDecimal total;
// Use built-in LocalDateSerializer from JavaTimeModule
@JsonSerialize(using = com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer.class)
@JsonDeserialize(using = com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer.class)
public LocalDate placedOn;
}
Field-level annotations take precedence over module-registered serializers. Use module registration for global defaults and field annotations to override specific fields.