Match URI deep links

For matching hierarchical URIs against patterns and extracting arguments, use UriDeepLinkMatcher. It relies on kotlinx.serialization to deserialize matched arguments into your key classes.

To create a UriDeepLinkMatcher, provide a pattern DeepLinkUri and the serializer for the corresponding key:

For non-hierarchical URIs or custom schemes (such as tel:), see Create custom deep link matchers.

Supported matching patterns

UriDeepLinkMatcher matches URIs based on their five components: scheme, authority, path, query, and fragment. The following sections describe the supported pattern syntax, argument placeholders, and matching rules for each component.

Scheme matching

If no scheme is present in the URI pattern, both http and https are matched. To match a specific scheme, include it in the pattern. As an exception, an http scheme in a pattern matches both http and https request URIs, while https in a pattern only matches https requests.

Pattern URI Request URI Match
www.example.com https://www.example.com
www.example.com http://www.example.com
http://www.example.com http://www.example.com
http://www.example.com https://www.example.com
https://www.example.com http://www.example.com
myapp://www.example.com myapp://www.example.com

Authority matching

UriDeepLinkMatcher performs a case-insensitive exact match on the URI authority (host and optional port). Placeholders or wildcards aren't supported in the authority, and no arguments are extracted:

Pattern URI Request URI Match
example.com https://example.com
example.com https://EXAMPLE.COM
example.com https://sub.example.com
example.com https://www.example.com
example.com https://example.com:8080
example.com:8080 https://example.com:8080
example.com:8080 https://example.com

Path matching

The following path patterns are supported:

Pattern URI Request URI Match Extracted Arguments
www.example.com/users https://www.example.com/users None
www.example.com/users/{id} https://www.example.com/users/123 id: "123"
www.example.com/users/{first}-{last} https://www.example.com/users/john-doe first: "john", last: "doe"
www.example.com/users/{id}/profile https://www.example.com/users//profile id: "" (Empty string)
www.example.com/users/user_{id} https://www.example.com/users/user_123 id: "123"
www.example.com/users/{userId}/posts/{postId} https://www.example.com/users/123/posts/456 userId: "123", postId: "456"
www.example.com/users/.* https://www.example.com/users/john-doe None
www.example.com/users https://www.example.com/users/ ❌ (Trailing slash creates an extra segment) N/A

Query matching

Query parameter order in the request URI doesn't need to match the order in the pattern URI. Additionally, parameters present in the request URI but not the pattern URI are ignored.

The following query parameter patterns are supported:

Pattern URI Request URI Extracted Arguments
www.example.com/users?name={name} https://www.example.com/users?name=john name: "john"
www.example.com/users?name={name} https://www.example.com/users?name= name: "" (Empty string)
www.example.com/users?{rawQuery} https://www.example.com/users?anything&else rawQuery: ["anything", "else"]
www.example.com/users?type=user_{id} https://www.example.com/users?type=user_123 id: "123"
www.example.com/users?name={first}_{last} https://www.example.com/users?name=john_doe first: "john", last: "doe"
www.example.com/users?list={list} https://www.example.com/users?list=10&list=20 list: ["10", "20"]
www.example.com/users?name={name}&{other} https://www.example.com/users?name=john&tab=info name: "john", other: ["tab=info"]
www.example.com/users?type=user_.* https://www.example.com/users?type=user_admin type: "admin"

Fragment matching

The following fragment pattern types are supported:

Pattern URI Request URI Extracted Arguments
www.example.com/#section1 https://www.example.com/#section1 None
www.example.com/#section_{id} https://www.example.com/#section_123 id: "123"
www.example.com/#section_.* https://www.example.com/#section_123 None

Supported data types

UriDeepLinkMatcher supports deserializing URI arguments into primitive types, enums, collections, and custom objects. Serialization falls into two categories:

  • Standard serialization: Uses kotlinx.serialization to deserialize into:
    • Primitives (Boolean, Int, Long, Float, Double, Char, Byte, Short) and String
    • Enums
    • Set, List, or Array of primitives, strings, or enums
    • Nested @Serializable classes (whose properties are flattened into individual URI placeholders)
  • Custom serialization with DeepLinkSerializer: Converts between a single String and custom objects, external types (such as java.time.LocalDate), or custom-delimited collections.

Standard serialization

UriDeepLinkMatcher works out of the box for standard types and flattened structures without requiring custom serializer implementations.

Primitives and strings

UriDeepLinkMatcher automatically decodes primitive types (Boolean, Int, Long, Float, Double, Char, Byte, Short) and String:

Enums

Enum values are matched case-sensitively against the enum element names:

Repeated query collections

Query parameters with repeated keys (such as ?id=10&id=20) automatically deserialize into List<T>, Set<T>, or Array<T> where T is a primitive type, String, or enum:

Nested @Serializable classes

When a NavKey contains a property whose type is another @Serializable class, UriDeepLinkMatcher flattens its properties so each property of the nested class maps directly to an individual URI parameter of the same name:

To deserialize custom objects (such as Filter(key = "brand", value = "pixel")), external types (such as java.time.LocalDate), or custom delimited strings (such as comma-separated values), extend DeepLinkSerializer<T>.

DeepLinkSerializer<T> is an abstract KSerializer<T> that converts between a String and T:

abstract class DeepLinkSerializer<T : Any> : KSerializer<T> {
    abstract val serialName: String
    abstract fun deserialize(value: String): T
    abstract fun serialize(value: T): String
}

For example, consider the Filter and FilterSerializer definitions that are used in the following snippets:

Single custom objects

To decode an object from a single URI parameter string (such as ?filter=brand:google), annotate the property with @Serializable(with = ...):

Custom objects in repeated query parameters

To deserialize repeated query parameters into a collection of custom objects (List<T>, Set<T>, or Array<T>), implement DeepLinkSerializer<T> for the element type T and annotate the type argument of the property with @Serializable(with = ...):

Delimited collections in single parameters

To parse comma-separated or custom-delimited values (such as ?ids=1,2,3) into a collection, implement DeepLinkSerializer for the entire collection type and annotate the property with @Serializable(with = ...):

Argument validation and matching outcomes

UriDeepLinkMatcher distinguishes between mismatches (returns null so other matchers can be attempted) and unsupported configurations (throws an exception).

Mismatches

A mismatch occurs when an incoming request URI doesn't satisfy the pattern or type requirements:

  • Missing required parameters: Non-nullable key properties without default values whose corresponding URI parameters are absent from the request URI.
  • Type parsing failures: Extracted argument values that can't be parsed into the expected property type (for example, "abc" for an Int property).

When a mismatch occurs, UriDeepLinkMatcher.match returns null, allowing subsequent matchers to be evaluated.

Consider a key class and matcher configured with default values, nested objects, and enums:

The following table demonstrates matching outcomes for various request URIs:

Request URI Decoding Outcome Match Result
https://www.example.com/map/paris?zoom=15&style=dark&layer=SATELLITE Success (All parameters provided) UriMatchResult(MapKey("paris", 15, LayerOptions("dark", MapLayer.SATELLITE)))
https://www.example.com/map/paris?style=dark Success (zoom defaults to 12, layer to STANDARD) UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD)))
https://www.example.com/map/paris?zoom=&style=dark Success (Empty optional query parameter uses default 12) UriMatchResult(MapKey("paris", 12, LayerOptions("dark", MapLayer.STANDARD)))
https://www.example.com/map?style=dark Mismatch (Missing required location parameter) null
https://www.example.com/map/paris?zoom=close&style=dark Mismatch ("close" isn't an Int) null
https://www.example.com/map/paris?style=dark&layer=HYBRID Mismatch ("HYBRID" isn't in enum) null

Unsupported configurations

If your key class contains unsupported data types, UriDeepLinkMatcher throws an exception during matching instead of returning null.

  • Maps and multi-dimensional collections: UriDeepLinkMatcher only supports single-dimensional collections of primitives, strings, enums, or custom types annotated with a DeepLinkSerializer. Map types throw an IllegalArgumentException, while nested collections (such as List<List<String>>) throw a SerializationException.
  • Unannotated custom object collections: Collections of custom types (such as List<Filter>) throw a SerializationException unless the element type is annotated with a DeepLinkSerializer.
  • Unflattened nested classes: Nested @Serializable classes can't be mapped to a single placeholder (such as ?user={user}) without a DeepLinkSerializer.

UriMatchResult comparison

UriMatchResult instances are ranked using the following criteria in order:

  1. MatchResult type: UriMatchResult ranks higher than other MatchResult types.
  2. Exact path: Literal path matches rank higher than placeholder or wildcard matches.
  3. Path argument count: Matches with more path arguments rank higher.
  4. Presence of arguments: Matches that capture arguments rank higher than those that don't.
  5. Total argument count: The total number of arguments (path, query, fragment) is the final tie-breaker.

Customize UriDeepLinkMatcher

UriDeepLinkMatcher is an open class that you can subclass to customize URI matching and argument extraction behavior:

  • matchRequest: Top-level matching entry point for an incoming DeepLinkRequest. Override this to inspect request extras or apply custom preconditions before URI matching.
  • matchUri: Matches the DeepLinkUri against the configured pattern. Override this to intercept and normalize incoming URIs (for example, rewriting dynamic subdomains or legacy path formats) before calling super.matchUri.
  • matchArguments: Deserializes the extracted path, query, and fragment argument maps into a navigation key instance using the provided serializer. Override this to inject dynamic values or transform arguments before key instantiation.

The following example demonstrates subclassing UriDeepLinkMatcher to normalize legacy URL path prefixes before matching: