square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
43.09k stars 7.3k forks source link

Maps do not support multi-values #1324

Open pawel-kaminski-krk opened 8 years ago

pawel-kaminski-krk commented 8 years ago

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?

JakeWharton commented 8 years ago

None of the maps support this currently, no.

pawel-kaminski-krk commented 8 years ago

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

liorzam commented 8 years ago

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

pawel-kaminski-krk commented 8 years ago

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

raniejade commented 8 years ago

@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.

arturdryomov commented 8 years ago

Most likely #1184 provides some help with this.

pawel-kaminski-krk commented 8 years ago

thanks! it seems will do the trick!

mandybess commented 8 years ago

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

jm-lim commented 6 years ago

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.

gzp-gnr commented 4 years ago

hi, is this supported in the lastest retrofit version ?

lgtout commented 4 years ago

@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
        }
}
sambit-m commented 4 years ago

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

fmmr commented 3 years ago

+1 on this issue. Please support Map<String, Iterable> (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> 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()
            }
    }