To convert an object to a Map in Java, we pass the object to Jackson’s convertValue() method with a TypeReference<Map<String, Object>>, and every property becomes a map entry, with nested objects turned into nested maps. Gson, the JavaPropsMapper class and plain Java reflection do the same job with different trade-offs.
We need object to Map conversion when code must work with any type of object, for example to write an audit log of changed fields, to fill a template, to store an object as a Redis hash or a document, or to compare two objects field by field.
The following example converts a Book record with Jackson 3.
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
JsonMapper mapper = new JsonMapper();
Map<String, Object> map = mapper.convertValue(book, new TypeReference<Map<String, Object>>() {});
Object pages = map.get("pages"); // 412
Object published = map.get("published"); // "1965-08-01"
Object author = map.get("author"); // {name=Herbert, country=US}
Notice that the LocalDate became a String and the nested Author became another Map. We look at nested values and null handling in Jackson, flatten the object into dot-separated keys, compare Jackson with Gson and reflection, and use the result for an audit log.
1. The Book and Author Records
All examples convert the same small library catalog model. A Book has simple values such as String and int, a LocalDate, a List of tags and a nested Author record, so we can see how each approach treats every kind of value.
public record Author(String name, String country) {
}
public record Book(String title, int pages, LocalDate published, List<String> tags, Author author) {
}
Records work with all four approaches. A regular class works the same way with Jackson as long as it has getters or public fields, as section 7.1 explains.
2. Using Jackson
Jackson is the most used JSON library in Java, and Spring Boot 4 auto-configures a Jackson 3 JsonMapper bean. Jackson converts an object to a Map in two ways, namely into a nested Map with convertValue(), or into flat dot-separated keys with the properties module.
The examples use Jackson 3.2.3, where the Maven group is tools.jackson.core and the packages start with tools.jackson. Jackson 3 needs Java 17 or later and supports java.time types without an extra module.
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.2.3</version>
</dependency>
2.1. Using convertValue()
The convertValue() method converts a value from one type to another by writing it into an in-memory token buffer and reading the result back as the target type. No JSON text is produced, so convertValue() skips the text formatting and parsing that writeValueAsString() followed by readValue() would do, and it gives the same result.

Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
JsonMapper mapper = new JsonMapper();
Map<String, Object> map = mapper.convertValue(book, new TypeReference<Map<String, Object>>() {});
String text = map.toString(); // "{title=Dune, pages=412, published=1965-08-01, tags=[sci-fi, classic], author={name=Herbert, country=US}}"
String mapType = map.getClass().getSimpleName(); // "LinkedHashMap"
String authorType = map.get("author").getClass().getSimpleName(); // "LinkedHashMap"
String tagsType = map.get("tags").getClass().getSimpleName(); // "ArrayList"
The result is a LinkedHashMap that keeps the declaration order of the record components. Numbers keep their Java type, so pages is an Integer, nested objects become LinkedHashMap instances and collections become ArrayList instances.
The same method converts the Map back into a Book, because Jackson can bind a Map to any type it can bind JSON to.
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
JsonMapper mapper = new JsonMapper();
Map<String, Object> map = mapper.convertValue(book, new TypeReference<Map<String, Object>>() {});
Book copy = mapper.convertValue(map, Book.class);
boolean same = copy.equals(book); // true
Always pass a TypeReference instead of Map.class, because the raw Map type compiles with an unchecked warning and hides the real value types.
2.2. Skipping Null Values
By default, a null property becomes a map entry with a null value. If we send the map to a store that rejects null, such as a Redis hash, we configure the mapper to leave those entries out.
Book draft = new Book("Draft", 0, null, List.of(), null);
JsonMapper mapper = new JsonMapper();
Map<String, Object> all = mapper.convertValue(draft, new TypeReference<Map<String, Object>>() {});
String allText = all.toString(); // "{title=Draft, pages=0, published=null, tags=[], author=null}"
JsonMapper nonNull = JsonMapper.builder()
.changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL))
.build();
Map<String, Object> some = nonNull.convertValue(draft, new TypeReference<Map<String, Object>>() {});
String someText = some.toString(); // "{title=Draft, pages=0, tags=[]}"
The Jackson inclusion settings also offer NON_EMPTY, which drops empty lists and strings as well.
2.3. Using JavaPropsMapper for Flat Keys
Some targets accept only flat key-value pairs with String values, such as a .properties file, HTTP query parameters or a metrics label set. The JavaPropsMapper class from the jackson-dataformat-properties module flattens nested objects into dot-separated keys and numbers list elements starting from 1.
<dependency>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-properties</artifactId>
<version>3.2.3</version>
</dependency>
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
JavaPropsMapper propsMapper = new JavaPropsMapper();
Map<String, String> flat = propsMapper.writeValueAsMap(book);
String flatText = flat.toString(); // "{title=Dune, pages=412, published=1965-08-01, tags.1=sci-fi, tags.2=classic, author.name=Herbert, author.country=US}"
String country = flat.get("author.country"); // "US"
Properties props = propsMapper.writeValueAsProperties(book);
String firstTag = props.getProperty("tags.1"); // "sci-fi"
Book back = propsMapper.readMapAs(flat, Book.class);
boolean same = back.equals(book); // true
We can see that every value is a String in the result, including pages. The writeValueAsMap() method returns a Map<String, String> in declaration order, whereas writeValueAsProperties() returns a Properties object that we can store as a file.
3. Using Gson’s TypeToken
If a project already uses Gson instead of Jackson, we convert the object to a JSON tree and read it back as a Map with a TypeToken. Gson has no convertValue() method, so this round trip is the standard way to do it.
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDate.class, (JsonSerializer<LocalDate>) (date, type, ctx) -> new JsonPrimitive(date.toString()))
.create();
Map<String, Object> map = gson.fromJson(gson.toJsonTree(book), new TypeToken<Map<String, Object>>() {}.getType());
String text = map.toString(); // "{title=Dune, pages=412.0, published=1965-08-01, tags=[sci-fi, classic], author={name=Herbert, country=US}}"
String mapType = map.getClass().getSimpleName(); // "LinkedTreeMap"
There are two differences from the Jackson result. Gson reads every number into an Object value as a Double, so 412 became 412.0, and nested objects become Gson’s own LinkedTreeMap instead of LinkedHashMap.
Without the LocalDate adapter, Gson 2.14 writes the private fields of LocalDate, and published turns into a map such as {year=1965.0, month=8.0, day=1.0}. The custom serializer keeps the ISO date text. To keep whole numbers as Long, we set a number policy on the builder.
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
Gson gson = new GsonBuilder()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.registerTypeAdapter(LocalDate.class, (JsonSerializer<LocalDate>) (date, type, ctx) -> new JsonPrimitive(date.toString()))
.create();
Map<String, Object> map = gson.fromJson(gson.toJsonTree(book), new TypeToken<Map<String, Object>>() {}.getType());
Object pages = map.get("pages"); // 412
String pagesType = pages.getClass().getSimpleName(); // "Long"
4. Using Reflection
Reflection needs no library, but we write and test every rule ourselves, such as static fields, inherited fields, null values and nested objects. For records, the getRecordComponents() method returns the components in declaration order, and each one has an accessor method that we can call without setAccessible().
static Map<String, Object> recordToMap(Record source) throws ReflectiveOperationException {
Map<String, Object> map = new LinkedHashMap<>();
for (RecordComponent component : source.getClass().getRecordComponents()) {
map.put(component.getName(), component.getAccessor().invoke(source));
}
return map;
}
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
Map<String, Object> map = recordToMap(book);
String text = map.toString(); // "{title=Dune, pages=412, published=1965-08-01, tags=[sci-fi, classic], author=Author[name=Herbert, country=US]}"
Object author = map.get("author"); // Author[name=Herbert, country=US]
Notice that the nested author stays an Author object, because the method converts only the top level. That is often what we want for a shallow copy of the values, and it is the main difference from Jackson.
For a regular class, we read the fields instead. The following version walks up the class hierarchy to include inherited fields, skips static fields and keeps null values, which Collectors.toMap() would reject with a NullPointerException.
static Map<String, Object> fieldsToMap(Object source) throws IllegalAccessException {
Map<String, Object> map = new LinkedHashMap<>();
for (Class<?> type = source.getClass(); type != Object.class; type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
map.putIfAbsent(field.getName(), field.get(source));
}
}
return map;
}
public static class Shelf {
private static int count = 0;
private final String room = "B2";
private final String label = null;
}
Map<String, Object> shelf = fieldsToMap(new Shelf());
String shelfText = shelf.toString(); // "{room=B2, label=null}"
The call to setAccessible(true) works for our own classes, but JDK classes such as String or LocalDate are in modules that do not open their packages, so the call throws InaccessibleObjectException on them. Jackson and Gson avoid that problem for java.time types with built-in or custom converters.
5. Comparing Ways to Convert an Object to a Map
The four approaches differ most in how they treat nested objects, numbers and dates, which matters as soon as the map leaves our code.
| Behavior | Jackson convertValue() | Jackson JavaPropsMapper | Gson round trip | Reflection |
|---|---|---|---|---|
| Nested objects | Nested LinkedHashMap | Flat keys such as author.name | Nested LinkedTreeMap | Original objects |
| Numbers | Keep Java type | String | Double by default | Keep Java type |
| LocalDate | ISO String | ISO String | Needs an adapter | Original LocalDate |
| null values | Kept, or skipped with NON_NULL | Empty String | Skipped by default | Kept |
| Map back to object | convertValue(map, Book.class) | readMapAs(), readPropertiesAs() | fromJson(tree, Book.class) | Hand-written |
| Dependency | jackson-databind | jackson-dataformat-properties | gson | None |
Jackson’s convertValue() is the best default because it keeps value types and converts in both directions. We pick JavaPropsMapper for flat String pairs, Gson only when the project already standardizes on it, and reflection when a shallow, dependency-free copy is enough.
6. Real-World Example of an Audit Log
Say a library app lets librarians edit book records, and every edit must be stored in an audit table with the names of the changed fields. Writing a comparison per record type does not scale, but two maps from convertValue() can be compared with the same few lines for any type.
Book before = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi"), new Author("Herbert", "US"));
Book after = new Book("Dune", 896, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
JsonMapper mapper = new JsonMapper();
TypeReference<Map<String, Object>> mapType = new TypeReference<>() {};
Map<String, Object> oldValues = mapper.convertValue(before, mapType);
Map<String, Object> newValues = mapper.convertValue(after, mapType);
Set<String> changed = oldValues.keySet().stream()
.filter(key -> !Objects.equals(oldValues.get(key), newValues.get(key)))
.collect(Collectors.toCollection(TreeSet::new));
String changedText = changed.toString(); // "[pages, tags]"
The nested author maps are equal, so the comparison does not report them. Because nested values are maps and lists, Objects.equals() compares them by content, which would not work with objects that do not override equals().
7. Object to Map FAQs
Real classes without getters, the reverse direction and older Jackson versions each change the conversion a little.
7.1. Why Does convertValue() Return an Empty Map?
Jackson finds no properties when a class has only private fields and no getters, so Jackson 3 returns {}. We add getters, turn the class into a record, or annotate it with @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) so Jackson reads the fields.
7.2. How Do We Convert a Map Back to an Object?
We call convertValue() in the other direction, as in mapper.convertValue(map, Book.class). Map keys must match the property names, and Jackson converts values such as the ISO date text back to LocalDate. The JSON to HashMap guide covers the same idea for JSON text.
7.3. How Do We Convert an Object to a Map Without a Library?
We use reflection, as in section 4. For records, getRecordComponents() gives a clean solution with no access tricks. For a few known classes, a hand-written toMap() method is faster and easier to read than either reflection or a library.
7.4. How Does Jackson 2 Differ From Jackson 3 for This Conversion?
The method is the same, but Jackson 2 uses com.fasterxml.jackson.databind.ObjectMapper, needs the jackson-datatype-jsr310 module for LocalDate, and writes dates as number arrays such as [1965, 8, 1] unless SerializationFeature.WRITE_DATES_AS_TIMESTAMPS is disabled. The Jackson 3 migration guide lists the other changes.
7.5. Can We Convert a List of Objects to a List of Maps?
Yes. We pass a TypeReference<List<Map<String, Object>>> to convertValue(), and Jackson converts each element.
Book book = new Book("Dune", 412, LocalDate.of(1965, 8, 1), List.of("sci-fi", "classic"), new Author("Herbert", "US"));
JsonMapper mapper = new JsonMapper();
List<Map<String, Object>> rows = mapper.convertValue(List.of(book), new TypeReference<List<Map<String, Object>>>() {});
Object title = rows.get(0).get("title"); // "Dune"
8. Conclusion
Jackson’s convertValue() with a TypeReference<Map<String, Object>> is the shortest and safest way to convert an object to a Map in Java. It keeps number types, turns nested objects into nested maps, formats java.time values as ISO text in Jackson 3, and converts the map back with the same method.
For flat String pairs, JavaPropsMapper builds dot-separated keys. Gson works too, as long as we handle its Double numbers and register date adapters, and reflection fits small, shallow conversions without a dependency. For more core topics, see the Java tutorial.
9. References
- Jackson 3.0 release notes
- Jackson ObjectMapper Javadoc
- Jackson properties module
- Gson ToNumberPolicy Javadoc
- Class.getRecordComponents() Javadoc
Happy Learning !!