skip to content

In Dart, how do you build a request Uri with query parameters safely, and why does Uri.https fail when a query value is an int?

level: middleimportance: should knowfreq 32%

answer

  1. Uri.https(authority, path, params)
  2. values: String, null or Iterable<String>
  3. keys and values encoded, space as +
  4. parse throws, tryParse returns null
  5. queryParametersAll for repeated keys

basics

~20 s

Build it with Uri.https(host, path, queryParameters) or the Uri() constructor, which percent-encode every key and value. Each value must be a String, null or an Iterable<String>, so an int fails at runtime; convert it with toString() first.

solid answer

~40 s

I never concatenate URLs by hand. `Uri.https(authority, unencodedPath, queryParameters)` and `Uri.http` build a URI from parts, and the general `Uri(scheme:, host:, path:, queryParameters:, fragment:)` constructor covers everything else. The query map is typed `Map<String, dynamic>`, but the documentation requires each value to be `null`, a `String` or an `Iterable<String>`; an iterable repeats the key, and anything else, such as an `int`, fails with a type error at runtime, so write `'$nights'`. Keys and values are percent-encoded with spaces turned into `+`. Going the other way, `Uri.parse` throws a `FormatException` on invalid text, `Uri.tryParse` returns `null`, `queryParameters` gives an unmodifiable map with one value per key, and `queryParametersAll` keeps repeated keys. `Uri.encodeComponent` and `encodeFull` handle one-off strings.

code

dart · 17 lines
dart
Uri availabilityUri(String city, DateTime checkIn, int nights) => Uri.https(
  'api.example.com',
  '/v1/availability',
  {
    'city': city, // 'São Paulo' is percent-encoded for you
    'from': checkIn.toUtc().toIso8601String(),
    'nights': '$nights', // an int here would fail at runtime
    'amenity': ['wifi', 'parking'], // repeated key
  },
);

void main() {
  final uri = availabilityUri('São Paulo', DateTime.utc(2026, 10, 24), 3);
  print(uri.queryParameters['city']); // São Paulo
  print(uri.queryParametersAll['amenity']); // [wifi, parking]
  print(Uri.tryParse('::Not valid URI::')); // null
}

go deeper

for a junior

Know to build URLs with Uri.https and a query map instead of string concatenation, and that values must be strings.

for a middle

Explain the value contract (String, null, Iterable<String>), + for spaces, parse versus tryParse, and queryParameters versus queryParametersAll.

for a senior

Catch double encoding, runtime type errors from dynamic maps, and lost repeated keys in review, and wrap URL building in one typed helper per API.

for a principal

Put endpoint construction behind a typed client layer so no feature builds URLs itself, which keeps encoding rules and base hosts in one place.

## Why not string concatenation Building `'https://api.example.com/v1/availability?city=' + city` breaks as soon as the city is `São Paulo` or contains `&`. The `Uri` class in `dart:core` builds URIs from parts and encodes each part correctly, and it parses URI strings back into parts. ## Building | API | Use it for | |---|---| | `Uri.https(authority, [unencodedPath, queryParameters])` | the common HTTPS request URL | | `Uri.http(...)` | the same with the `http` scheme | | `Uri(scheme:, host:, port:, path:, query:, queryParameters:, fragment:)` | anything else, including a fragment or a custom scheme | | `uri.replace(queryParameters: {...})` | a copy with some parts changed | `Uri.https` takes the authority, host with an optional port or user info, then a path that it percent-encodes, so `'a b'` becomes `a%20b` and a literal `%` becomes `%25`. The path may be omitted. ## The query map's contract The parameter is typed `Map<String, dynamic>`, which the compiler cannot check further, so the rule lives in the documentation and the runtime: - A value must be **`null`, a `String` or an `Iterable<String>`**. - An **iterable** produces the key once per element, which is how APIs take repeated keys such as `amenity=wifi&amenity=parking`. - **`null`** or an empty iterable gives the key with no value. - Any other value, such as an `int`, a `bool` or a `DateTime`, is treated as an iterable and fails with a **type error at runtime**. Convert it first: `'$nights'`, `checkIn.toUtc().toIso8601String()`. - Every key and value is **percent-encoded** except unreserved characters, and **spaces become `+`**, the form-encoding convention. - You cannot pass both `query` and `queryParameters`; doing so throws an `ArgumentError`. ## Parsing and reading - `Uri.parse(text)` throws a `FormatException` when the text is not a valid URI or URI reference; `Uri.tryParse(text)` returns `null` instead. - `uri.scheme`, `host`, `port`, `path`, `pathSegments` and `fragment` expose the parts. - `uri.queryParameters` is an **unmodifiable** `Map<String, String>` with decoded values; if a key repeats, one of its values is chosen arbitrarily. - `uri.queryParametersAll` maps each key to **all** of its values. ## Encoding single strings When you only need to encode one piece, for example for a deep-link token: 1. `Uri.encodeComponent(s)` encodes everything that has special meaning in a URI, including `/`, `&`, `:`; use it for one path segment or value. 2. `Uri.encodeFull(s)` leaves the URI's structural characters alone and encodes the rest; use it for a whole URL that only has stray spaces. 3. `Uri.decodeComponent` and `Uri.decodeFull` reverse them. Prefer building with `Uri.https` over encoding by hand; the constructor chooses the right encoding per part. ## Reading an incoming deep link The same class parses links the app receives, for example a confirmation email linking to `https://book.example.com/stay/LIS-0042?guests=2&tag=a&tag=b`: - `uri.pathSegments` gives `['stay', 'LIS-0042']`, already split and decoded. - `uri.queryParameters['guests']` gives `'2'`, a string, so it still needs `int.tryParse`. - `uri.queryParametersAll['tag']` gives `['a', 'b']`. - A missing key returns `null` from the map, so every read needs a fallback. Routing packages parse links for you, but the values they hand over are these same decoded strings. ## A booking example Searching availability needs a city, a check-in instant and a number of nights, plus repeated amenities. With `Uri.https` the city `São Paulo` is encoded, the timestamp's colons are escaped, `nights` is converted to a string first, and the amenity list becomes two `amenity` entries. The resulting `Uri` is what HTTP client packages take as their URL argument; those packages are a separate topic. ## Mistakes interviewers look for - Concatenating user input into a URL string. - Passing an `int` or `DateTime` as a query value and meeting a runtime type error. - Reading `queryParameters` on a URL with repeated keys and losing values. - Using `Uri.parse` on user-typed text without handling `FormatException`, where `tryParse` fits. - Encoding a value twice, once by hand with `encodeComponent` and again by the constructor, producing `%2520`.

  • What is the difference between Uri.encodeComponent and Uri.encodeFull?
    `encodeComponent` escapes every character with special meaning in a URI, including `/`, `:` and `&`, so it suits one path segment or query value. `encodeFull` leaves those structural characters alone and escapes the rest, so it suits a complete URL that only contains stray spaces. When building with `Uri.https` you need neither.
  • A deep link arrives as ?tag=a&tag=b. What does uri.queryParameters['tag'] return?
    One of the two values, chosen arbitrarily, because `queryParameters` maps each key to a single string. Use `uri.queryParametersAll['tag']`, which returns `['a', 'b']`.

saying these in an interview costs you the question

  • Uri.https converts int query values with toString() automatically
  • Uri.parse returns null for invalid text
  • queryParameters returns every value of a repeated key
  • Encode values with Uri.encodeComponent before passing them to Uri.https
  • Building URLs with string interpolation is fine if you trim the input