retrofit icon indicating copy to clipboard operation
retrofit copied to clipboard

Maps do not support multi-values

Open pawel-kaminski-krk opened this issue 8 years ago • 13 comments

hi, we are using retrofit 1.9 and we declared our endpoint as

@GET("/path")
Response get(@QueryMap Map<String, String> queryParams);

which works fine and is expanded to ex. host/path?rq=1&ra=2 for input {"rq": "1", "ra": "2"}.

right now we need to support multivalue same as described here https://futurestud.io/blog/retrofit-multiple-query-parameters-of-same-name but for @QueryMap. and declaring endpoint as

@GET("/path")
Response get(@QueryMap Map<String, List<String>> queryParams);

doesn't help at all as value is expanded to its string equivalent ex. host/path?rq=[1,2] for input {"rq": ["1", "2"]}. we would like to get same behavior as for @Query ex. host/path?rq=1&rq=2 for input {"rq": ["1", "2"]}.

is there any work around for now?

pawel-kaminski-krk avatar Nov 25 '15 15:11 pawel-kaminski-krk

None of the maps support this currently, no.

JakeWharton avatar Nov 25 '15 17:11 JakeWharton

is there any road map. we solve the issue adding another parameter with @Query annotation which supports multi value. but this is ugly

pawel-kaminski-krk avatar Nov 26 '15 09:11 pawel-kaminski-krk

In my project I serialized my object via GSON to string and with @Query annotation I succeed to send it probably

liorzam avatar Nov 26 '15 17:11 liorzam

it is not about serializing but properly handling multivalues for one key

pawel-kaminski-krk avatar Dec 02 '15 12:12 pawel-kaminski-krk

@PartMap does not support multi values as well. One trick around it is to extend Map and overriding Map#entrySet(), though this can break as it depends on the internals.

raniejade avatar Dec 10 '15 00:12 raniejade

Most likely #1184 provides some help with this.

arturdryomov avatar Jan 04 '16 17:01 arturdryomov

thanks! it seems will do the trick!

pawel-kaminski-krk avatar Jan 05 '16 12:01 pawel-kaminski-krk

in the meantime, i hacked this together for anyone who absolutely needs this and can't wait - https://gist.github.com/mandybess/dca2e8a0527aff2d8e0688c17297c945

mandybess avatar Jul 27 '16 02:07 mandybess

I have a simple trick.

public class ProxyRetrofitQueryMap extends HashMap<String, Object> {
    public ProxyRetrofitQueryMap(Map<String, Object> m) {
        super(m);
    }

    @Override
    public Set<Entry<String, Object>> entrySet() {
        Set<Entry<String, Object>> originSet = super.entrySet();
        Set<Entry<String, Object>> newSet = new HashSet<>();

        for (Entry<String, Object> entry : originSet) {
            String entryKey = entry.getKey();
            if (entryKey == null) {
                throw new IllegalArgumentException("Query map contained null key.");
            }
            Object entryValue = entry.getValue();
            if (entryValue == null) {
                throw new IllegalArgumentException(
                        "Query map contained null value for key '" + entryKey + "'.");
            }
            else if(entryValue instanceof List) {
                for(Object arrayValue:(List)entryValue)	 {
                    if (arrayValue != null) { // Skip null values
                        Entry<String, Object> newEntry = new AbstractMap.SimpleEntry<>(entryKey, arrayValue);
                        newSet.add(newEntry);
                    }
                }
            }
            else {
                Entry<String, Object> newEntry = new AbstractMap.SimpleEntry<>(entryKey, entryValue);
                newSet.add(newEntry);
            }
        }
        return newSet;
    }
}

and use that class => @Querymap or @FieldMap.

jm-lim avatar Mar 05 '18 06:03 jm-lim

hi, is this supported in the lastest retrofit version ?

gzp-gnr avatar Apr 27 '20 16:04 gzp-gnr

@jm-lim's solution, converted for Kotlin:

class ProxyRetrofitQueryMap(m: MutableMap<String, Any>) : HashMap<String, Any>(m) {
    override val entries: MutableSet<MutableMap.MutableEntry<String, Any>>
        get() {
            val originSet: Set<Map.Entry<String?, Any?>> = super.entries
            val newSet: MutableSet<MutableMap.MutableEntry<String, Any>> = HashSet()
            for ((key, entryValue) in originSet) {
                val entryKey = key ?: throw IllegalArgumentException("Query map contained null key.")
                // Skip null values
                requireNotNull(entryValue) { "Query map contained null value for key '$entryKey'." }
                if (entryValue is List<*>) {
                    for (arrayValue in entryValue) {
                        if (arrayValue != null) { // Skip null values
                            val newEntry: MutableMap.MutableEntry<String, Any> =
                                    SimpleEntry(entryKey, arrayValue)
                            newSet.add(newEntry)
                        }
                    }
                } else {
                    val newEntry: MutableMap.MutableEntry<String, Any> = SimpleEntry(entryKey, entryValue)
                    newSet.add(newEntry)
                }
            }
            return newSet
        }
}

lgtout avatar May 13 '20 00:05 lgtout

Any update on this or should we go with custom solution? @JakeWharton

sambit-m avatar Aug 28 '20 12:08 sambit-m

+1 on this issue. Please support Map<String, Iterable<Object>> (or any of guava, spring, commons-collections multimaps) as a parameter annotated with @QueryMap

btw - if your input already has data on the form Map<String, List<String>> the solution of @lgtout and @jm-lim could be reduced to something like:

    private class ProxyRetrofitQueryMap(val original: Map<String, List<String>>) : AbstractMap<String, String>() {
        override val entries: Set<Map.Entry<String, String>>
            get() {
                return original.entries.flatMap { (key, value) -> value.map { SimpleEntry(key, it) } }.toSet()
            }
    }

fmmr avatar Dec 10 '20 15:12 fmmr