Parsing URL query string in Kotlin

Parsing URL query into a map

Sometimes you may need to parse a query string into a key-value map.

Someone will suggest you to use a library like Apache, but I think it’s too easy task to add a library for it.

Many solutions (on StackOverflow, for example) don’t take into account that URL query string can have multiple values for the same key, so their implementations return Map<String, String> instead of Map<String, Set<String>.

So, let’s see my solution for that problem:

internal interface ParseQueryParams {
    fun exec(queryString: String): Map<String, List<String>>
}

internal class ParseQueryParamsImpl @Inject constructor() : ParseQueryParams {

    override fun exec(queryString: String): Map<String, List<String>> {
        return queryString
            .split(QUERY_PARAMS_SEPARATOR)
            .mapNotNull { keyAndValue ->
                val keyAndValueList = keyAndValue.split(KEY_VALUE_SEPARATOR)
                keyAndValueList.takeIf { it.size == 2 }
            }
            .mapNotNull { keyAndValueList ->
                val (key, value) = keyAndValueList
                (key to value.trim()).takeIf { value.isNotBlank() } // Don't take empty params
            }
            .groupBy { (key, _) -> key }
            .map { mapEntry ->
                mapEntry.key to mapEntry.value.map { it.second }
            }
            .toMap()
    }

    private companion object {
        private const val QUERY_PARAMS_SEPARATOR = "&"
        private const val KEY_VALUE_SEPARATOR = "="
    }
}

For example, we will work with this query string: param1=42&param2=54&param3=ololo&param3=trololo&param4=&param5

First of all, we split the query string to list of key=value strings:

queryString.split(QUERY_PARAMS_SEPARATOR)

Outcome of this step:

param1=42
param2=54
param3=ololo
param3=trololo
param4=
param5

Next, split key and value and take the result only if it’s two elements long:

.mapNotNull { keyAndValue ->
    val keyAndValueList = keyAndValue.split(KEY_VALUE_SEPARATOR)
    keyAndValueList.takeIf { it.size == 2 }
}

Outcome of this step:

[param1, 42]
[param2, 54]
[param3, ololo]
[param3, trololo]
[param4, ""]

Next, convert list of lists to list of pairs, filtering out blank values and trimming them:

.mapNotNull { keyAndValueList ->
    val (key, value) = keyAndValueList
    (key to value.trim()).takeIf { value.isNotBlank() }
}

Outcome of this step:

param1 to 42
param2 to 54
param3 to ololo
param3 to trololo

Group the values by their keys:

.groupBy { (key, _) -> key }

Outcome of this step:

param1 to (param1 to [42])
param2 to (param2 to [54])
param3 to (param3 to [ololo, trololo])

And then flatten the map values and convert the result to a map:

.map { mapEntry ->
    mapEntry.key to mapEntry.value.map { it.second }
}
.toMap()

Outcome of this step:

param1 to [42]
param2 to [54]
param3 to [ololo, trololo]

Bonus

Here is a JUnit test for this implementation:

internal class ParseQueryParamsTest {

    private val parseQueryParams: ParseQueryParams = ParseQueryParamsImpl()

    @Test
    fun `empty query string`() {
        val result = parseQueryParams.exec("")
        assertTrue(result.isEmpty())
    }

    @Test
    fun `invalid query string`() {
        val result = parseQueryParams.exec("ololo")
        assertTrue(result.isEmpty())
    }

    @Test
    fun `valid query string`() {
        val result = parseQueryParams.exec("param1=42&param2=54&param3=ololo&param3=trololo&param4=")
        assertEquals(3, result.size)

        val param1 = result["param1"]
        assertNotNull(param1)
        assertEquals(1, param1?.size)
        assertEquals("42", param1?.first())

        val param2 = result["param2"]
        assertNotNull(param2)
        assertEquals(1, param2?.size)
        assertEquals("54", param2?.first())

        val param3 = result["param3"]
        assertNotNull(param3)
        assertEquals(2, param3?.size)
        assertEquals("ololo", param3?.get(0))
        assertEquals("trololo", param3?.get(1))

        val param4 = result["param4"]
        assertNull(param4)
    }
}
, ,

Leave a Reply

Your email address will not be published. Required fields are marked *