46

I have a Java object obj that has attributes obj.attr1, obj.attr2 etc. The attributes are possibly accessed through an extra level of indirection: obj.getAttr1(), obj.getAttr2(), if not public.

The challenge: I want a function that takes an object, and returns a Map<String, Object>, where the keys are strings "attr1", "attr2" etc. and values are the corresponding objects obj.attr1, obj.attr2. I imagine the function would be invoked with something like

  • toMap(obj),
  • or toMap(obj, "attr1", "attr3") (where attr1 and attr3 are a subset of obj's attributes),
  • or perhaps toMap(obj, "getAttr1", "getAttr3") if necessary.

I don't know much about Java's introspection: how do you do that in Java?

Right now, I have a specialized toMap() implementation for each object type that I care about, and it's too much boilerplate.


NOTE: for those who know Python, I want something like obj.__dict__. Or dict((attr, obj.__getattribute__(attr)) for attr in attr_list) for the subset variant.

Radim
  • 4,208
  • 3
  • 27
  • 38

7 Answers7

66

Another way to user JacksonObjectMapper is the convertValue ex:

 ObjectMapper m = new ObjectMapper();
 Map<String,Object> mappedObject = m.convertValue(myObject, new TypeReference<Map<String, String>>() {});
Klesun
  • 12,280
  • 5
  • 59
  • 52
Endeios
  • 911
  • 1
  • 12
  • 12
  • This answer succintly and expertly solves the original problem. Nice job! – Jake Toronto Apr 17 '15 at 14:37
  • What is the gradle dependency for Android? – Shajeel Afzal May 30 '16 at 11:00
  • 5
    The problem I met with this solution is that it converts the object recursively, potentially destroying types in deeper object compositions (for example, Date instance in the 3rd level will be converted to Long, since this is what ObjectMapper does by default). Since I needed the method for data canonicalization, this was an issue... – Petr Dvořák Aug 07 '16 at 23:31
  • 1
    Is there a way to pass Type safety warning? – Ori Marko Aug 05 '19 at 07:18
  • 1
    The gradle dependency that I used for that solution is: `implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.9'` Hope It Helps! – hfunes.com Aug 23 '19 at 16:16
  • It is a VERY bad solution, the ObjectMapper convertValue() retains the class of very few Objects and the list of these types is unknown. convertValue() is the example of the worst possible programming. You will see no problems at the compile stage, but later you will struggle with clandestine errors. – Gangnus Feb 24 '20 at 13:19
  • A small improvement, for any kind, just change types but keep in mind implementing: Serializable interface and .hashCode() and .equals() methods: ```java mapper.convertValue(myObject, new TypeReference>() {}); ```. – Felix Aballi Apr 21 '20 at 21:59
  • When I did this conversion, an extra property appears in the map which looks like - `new -> false`. What is this for? – iamcrypticcoder Jun 09 '20 at 12:33
48

Use Apache Commons BeanUtils: http://commons.apache.org/beanutils/.

An implementation of Map for JavaBeans which uses introspection to get and put properties in the bean:

Map<Object, Object> introspected = new org.apache.commons.beanutils.BeanMap(object); 

Note: despite the fact the API returns Map<Object, Object> (since 1.9.0), the actual class for keys in the returned map is java.lang.String

Andrey
  • 6,526
  • 3
  • 39
  • 58
  • 1
    It seems this solution is only one handling 'put' method so far :) – Andrey Jul 22 '11 at 21:39
  • this creates `Map`, not `Map – vvondra Mar 27 '16 at 20:56
  • @vvondra Thanks, at the time of writing it was returning typeless Map, so the code was producing an unchecked assignment warning. In 1.9.0 they have changed it to Map, but in fact it always contains String only keys. – Andrey Mar 29 '16 at 03:48
  • 1
    it sucks, they did it to keep BC when adding generics in 1.9 http://commons.apache.org/proper/commons-beanutils/javadocs/v1.9.2/RELEASE-NOTES.txt – vvondra Mar 29 '16 at 08:55
  • 1
    The problem is that BeanMap object you are getting 1) does not implement the interface Map. and 2) has an excessive entry "Class"->"class.name.with.package". The first example from https://www.programcreek.com/java-api-examples/?api=org.apache.commons.beanutils.BeanMap can serve well for both problems. – Gangnus Feb 24 '20 at 13:14
  • @Gangnus your version may be worth a separate answer, as it is functionally different. E.g. will not update the object when put is invoked, handles getClass differently. 1) BeanMap does implement Map though (but does not extend HashMap). 2) Regarding Object#getClass() method, as technically it's a getter - I can see use cases when user wouldn't want that in the resulting map. – Andrey Feb 24 '20 at 16:09
  • @Andrey Yes, according to the javadoc, BeanMap implements Map. But if you try to use remove(), you have "not supported" message in runtime. So, REALLY it does not support Map. Why do you think, people use so complicate way to remove "class" entry? By remove() it would work in one line. And because of that I think that your answer plus referenced removing of class is the best answer. – Gangnus Feb 25 '20 at 00:49
  • @Gangnus remove is optional method in Map, and in such case it must throw UnsupportedOperationException by the java specification. Please note that the BeanMap is backed by the actual object. Modifications to the BeanMap will change the underlying object. To implement remove in such design it would require possibility to remove methods from the underlying object/class. When you move content to your own HashMap - you new map is detached from the object, and putting something to this new map will not update the original object. – Andrey Feb 26 '20 at 05:02
  • @Andrey If BeanMap does not support some method of an implemented interface, it should not support it at all. Interfaces allow us to catch errors at the compile time. And such politics makes errors invisible when compiling and makes us look into the process that can run under server or be included even deeper. It is an ugly antipattern. Thank you very much for noticing it. I would try to immediately transfer BeanMap into some normal map, as it is done in the example. – Gangnus Feb 27 '20 at 12:57
  • @Gagnus BeanMap is compliant with Java Map specification, remove is an optional method (https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#remove-java.lang.Object-). BeanMap represents the underlying object, and it won't allow to do something which results in illegal operation on the underlying object (e.g. remove something, which means removing getClass() or other method from object, and that is illegal in java). If it does not suit your use case - you don't have to use BeanMap. You can post a separate answer for other people having similar use case to yours. – Andrey Feb 27 '20 at 21:09
  • @Andrey Your answer is OK for me. We already have it in the master branch. (thanks again)What I do further with the gotten result, I choose at my will, don't I :-) ? ... The fact that something is used in Java does not prove that this way is good. They have enough antipatterns. And one of the main roles of SO is to find such stuff. It is easy to work with an ideal language and we wouldn't need help with such. If java were ideal, it wouldn't have the tag here. – Gangnus Mar 02 '20 at 23:20
30

You can use JavaBeans introspection for this. Read up on the java.beans.Introspector class:

public static Map<String, Object> introspect(Object obj) throws Exception {
    Map<String, Object> result = new HashMap<String, Object>();
    BeanInfo info = Introspector.getBeanInfo(obj.getClass());
    for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
        Method reader = pd.getReadMethod();
        if (reader != null)
            result.put(pd.getName(), reader.invoke(obj));
    }
    return result;
}

Big caveat: My code deals with getter methods only; it will not find naked fields. For fields, see highlycaffeinated's answer. :-) (You will probably want to combine the two approaches.)

C. K. Young
  • 219,335
  • 46
  • 382
  • 435
  • Whats the difference between Introspector vs just reflection? – Amir Raminfar Jul 22 '11 at 21:30
  • @Amir: Introspection uses reflection to do its work, but works at a higher level than reflection. Reflection finds Java-level fields and methods; introspection finds JavaBeans-level properties and events. – C. K. Young Jul 22 '11 at 21:42
  • All the answers were good. I'll accept this one because it proved the most useful to me in my final solution (which involves `BeanInfo` and `java.io.Serializable`). Thanks all. – Radim Jul 23 '11 at 13:04
17

Here's a rough approximation, hopefully enough to get you pointed in the right direction:

public Map<String, Object> getMap(Object o) {
    Map<String, Object> result = new HashMap<String, Object>();
    Field[] declaredFields = o.getClass().getDeclaredFields();
    for (Field field : declaredFields) {
        result.put(field.getName(), field.get(o));
    }
    return result;
}
highlycaffeinated
  • 19,729
  • 9
  • 60
  • 91
5

Here is a really easy way to do this.

Use Jackson JSON lib to convert the object to JSON.

Then read the JSON and convert it to a Map.

The map will contain everything you want.

Here is the 4 liner

ObjectMapper om = new ObjectMapper();
StringWriter sw = new StringWriter();
om.writeValue(object, sw);
Map<String, Object> map = om.readValue(sw.toString(), Map.class);

And additional win of course is that this is recursive and will create maps of maps if it needs to

Amir Raminfar
  • 33,777
  • 7
  • 93
  • 123
3

None of these work for nested properties, object mapper does a fair job except that you have to set all values on all fields you want to see in map and even then you cannot avoid/ignore objects own @Json annotations easily in ObjectMapper basically skip some of the properties. So unfortunately, you have to do something like the following, it is only a draft to just give an idea.

/*
     * returns fields that have getter/setters including nested fields as
     * field0, objA.field1, objA.objB.field2, ... 
     * to take care of recursive duplicates, 
     * simply use a set<Class> to track which classes
     * have already been traversed
     */
    public static void getBeanUtilsNestedFields(String prefix, 
            Class clazz,  List<String> nestedFieldNames) throws Exception {
        PropertyDescriptor[] descriptors = BeanUtils.getPropertyDescriptors(clazz);
        for(PropertyDescriptor descr : descriptors){
            // if you want values, use: descr.getValue(attributeName)
            if(descr.getPropertyType().getName().equals("java.lang.Class")){
                continue;
            }
            // a primitive, a CharSequence(String), Number, Date, URI, URL, Locale, Class, or corresponding array
            // or add more like UUID or other types
            if(!BeanUtils.isSimpleProperty(descr.getPropertyType())){
                Field collectionfield = clazz.getDeclaredField(descr.getName());
                if(collectionfield.getGenericType() instanceof ParameterizedType){
                    ParameterizedType integerListType = (ParameterizedType) collectionfield.getGenericType();
                    Class<?> actualClazz = (Class<?>) integerListType.getActualTypeArguments()[0];
                    getBeanUtilsNestedFields(descr.getName(), actualClazz, nestedFieldNames);
                }
                else{   // or a complex custom type to get nested fields
                    getBeanUtilsNestedFields(descr.getName(), descr.getPropertyType(), nestedFieldNames);
                }
            }
            else{
                nestedFieldNames.add(prefix.concat(".").concat(descr.getDisplayName()));
            }
        }
    }
kisna
  • 2,869
  • 1
  • 25
  • 30
0

maven dependencies

    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
    </dependency>

....

ObjectMapper m = new ObjectMapper();
Map<String,Object> mappedObject = m.convertValue(myObject,Map.class);

for JSR310 New Date/Time API,there are some issue need to be improved eg:

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.Test;

import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;

@Data
@NoArgsConstructor
public class QueryConditionBuilder
{
    LocalDateTime startTime;
    LocalDateTime endTime;
    Long nodeId;
    Long fsId;
    Long memId;
    Long ifCardId;

    private QueryConditionBuilder(QueryConditionBuilder.Builder builder) {
        setStartTime(builder.startTime);
        setEndTime(builder.endTime);
        setNodeId(builder.nodeId);
        setFsId(builder.fsId);
        setMemId(builder.memId);
        setIfCardId(builder.ifCardId);
    }

    public static QueryConditionBuilder.Builder newBuilder() {
        return new QueryConditionBuilder.Builder();
    }

    public static QueryConditionBuilder newEmptyBuilder() {
        return new QueryConditionBuilder.Builder().build();
    }


    public Map<String,Object> toFilter()
    {
        Map<String,Object> filter = new ObjectMapper().convertValue(this,Map.class);
        System.out.printf("查询条件:%s\n", JSON.toJSONString(filter));
        return filter;
    }

    public static final class Builder {
        private LocalDateTime startTime;
        private LocalDateTime endTime;
        private Long nodeId = null;
        private Long fsId = null;
        private Long memId =null;
        private Long ifCardId = null;

        private Builder() {
        }

        public QueryConditionBuilder.Builder withStartTime(LocalDateTime val) {
            startTime = val;
            return this;
        }

        public QueryConditionBuilder.Builder withEndTime(LocalDateTime val) {
            endTime = val;
            return this;
        }

        public QueryConditionBuilder.Builder withNodeId(Long val) {
            nodeId = val;
            return this;
        }

        public QueryConditionBuilder.Builder withFsId(Long val) {
            fsId = val;
            return this;
        }

        public QueryConditionBuilder.Builder withMemId(Long val) {
            memId = val;
            return this;
        }

        public QueryConditionBuilder.Builder withIfCardId(Long val) {
            ifCardId = val;
            return this;
        }

        public QueryConditionBuilder build() {
            return new QueryConditionBuilder(this);
        }
    }

    @Test
    public void test()
    {     
        LocalDateTime now = LocalDateTime.now(ZoneId.of("+8"));
        LocalDateTime yesterday = now.plusHours(-24);

        Map<String, Object> condition = QueryConditionBuilder.newBuilder()
                .withStartTime(yesterday)
                .withEndTime(now)
                .build().toFilter();

        System.out.println(condition);
    }
}

expects(pseudo-code):

查询条件:{"startTime":{"2019-07-15T20:43:15"},"endTime":{"2019-07-16T20:43:15"}
{startTime={2019-07-15T20:43:15}, endTime={"2019-07-16T20:43:15"}, nodeId=null, fsId=null, memId=null, ifCardId=null}

instead,i got these:

查询条件:{"startTime":{"dayOfMonth":15,"dayOfWeek":"MONDAY","dayOfYear":196,"hour":20,"minute":38,"month":"JULY","monthValue":7,"nano":263000000,"year":2019,"second":12,"chronology":{"id":"ISO","calendarType":"iso8601"}},"endTime":{"dayOfMonth":16,"dayOfWeek":"TUESDAY","dayOfYear":197,"hour":20,"minute":38,"month":"JULY","monthValue":7,"nano":263000000,"year":2019,"second":12,"chronology":{"id":"ISO","calendarType":"iso8601"}}}
{startTime={dayOfMonth=15, dayOfWeek=MONDAY, dayOfYear=196, hour=20, minute=38, month=JULY, monthValue=7, nano=263000000, year=2019, second=12, chronology={id=ISO, calendarType=iso8601}}, endTime={dayOfMonth=16, dayOfWeek=TUESDAY, dayOfYear=197, hour=20, minute=38, month=JULY, monthValue=7, nano=263000000, year=2019, second=12, chronology={id=ISO, calendarType=iso8601}}, nodeId=null, fsId=null, memId=null, ifCardId=null}

after a few research,an effective trick was found,

ObjectMapper mapper = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
//https://github.com/networknt/light-4j/issues/82
mapper.registerModule(module);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
//incase of empty/null String
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
Map<String,Object> filter = mapper.convertValue(this,Map.class);
System.out.printf("查询条件:%s\n", JSON.toJSONString(filter));
return filter;

output:

查询条件:{"startTime":"2019-07-15T21:29:13.711","endTime":"2019-07-16T21:29:13.711"}
{startTime=2019-07-15T21:29:13.711, endTime=2019-07-16T21:29:13.711, nodeId=null, fsId=null, memId=null, ifCardId=null}

I used the above code for dynamical query in MyBatis
eg.

 /***
     * 查询文件系统使用率
     * @param condition
     * @return
     */
    LinkedList<SnmpFileSystemUsage> queryFileSystemUsage(Map<String,Object> condition);

    List<SnmpFileSystemUsage> fooBar()
    { 
       return snmpBaseMapper.queryFileSystemUsage(QueryConditionBuilder
                .newBuilder()
                .withNodeId(nodeId)
                .build()
                .toFilter());
    }