SituationSolution
Format a BigDecimal as a plain number string without scientific notationCustom serializer
Deserialize a legacy API that sends booleans as "Y"/"N"Custom deserializer
Serialize a third-party class you cannot annotateCustom serializer + SimpleModule
Flatten or restructure nested JSON on readCustom deserializer
Write a type discriminator field during serializationCustom serializer
Simple field renaming / date formattingAnnotations (@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);
MethodWrites
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.