typed_deep_links

Router-neutral, type-safe deep links for Dart and Flutter. Declare routes on immutable models; generated code parses URIs, validates values, builds canonical URIs, and exposes a typed registry plus route manifest.

Install

dart pub add typed_deep_links
dart pub add --dev typed_deep_links_generator build_runner

Flutter apps can optionally add the Router API bridge:

flutter pub add typed_deep_links_flutter

Quick start

import 'package:typed_deep_links/typed_deep_links.dart';

part 'links.g.dart';

sealed class AppLink {}

@DeepLink('/orders/:id', aliases: ['/purchases/:id'])
final class OrderLink implements AppLink {
  const OrderLink({required this.id, this.source});

  @PathParameter.config(validation: DeepLinkValidation(min: 1))
  final int id;

  @QueryParameter('source')
  final String? source;
}

@DeepLinkRegistry(
  routes: [OrderLink],
  name: 'AppLinks',
  baseType: AppLink,
)
final class AppLinkRegistry {}

Generate once, or use watch while developing:

dart run build_runner build
final AppLink link = AppLinks.parse(
  Uri.parse('/orders/42?source=email'),
);

switch (link) {
  case OrderLink(:final id, :final source):
    openOrder(id, source: source);
}

const OrderLink(id: 42).toUri(); // /orders/42
AppLinks.toUri(link);

If a library has routes but no @DeepLinkRegistry, a backward-compatible DeepLinks registry returning Object is generated automatically.

Route features

Paths and migrations

@DeepLink(
  '/products/:slug/:tab?',
  aliases: ['/catalog/:slug/:tab?'],
  strictQueryParameters: true,
)
  • :name is a required path segment.
  • :name? is a trailing optional segment; its field must be nullable or have a constructor default.
  • *name is a final catch-all segment backed by String, List<String>, or Set<String>.
  • aliases are parse-only migration paths. toUri() always emits the canonical path.
  • A path ending in / appends remaining unannotated constructor fields, so @DeepLink('/orders/') still maps an id field to /orders/42.

Static routes win over dynamic routes. Ambiguous route shapes fail during code generation instead of depending on declaration order.

Query values, fragments, and enums

enum SortOrder {
  @DeepLinkEnumValue('best-match')
  relevance,

  @DeepLinkEnumValue('newest')
  recent,
}

@DeepLink('/search', strictQueryParameters: true)
final class SearchLink {
  const SearchLink({this.query, this.tags = const [], this.sort, this.section});

  @QueryParameter('q')
  final String? query;

  @QueryParameter()
  final List<String> tags; // repeated as ?tags=a&tags=b

  @QueryParameter()
  final SortOrder? sort;

  @FragmentParameter()
  final String? section;
}

Supported built-in scalar types are String, int, double, num, bool, DateTime, Uri, and enums. Query values also support List<T> and Set<T>. Boolean decoding accepts true, false, 1, and 0.

Custom value objects

final class ProductIdCodec implements DeepLinkValueCodec<ProductId> {
  const ProductIdCodec();

  @override
  ProductId decode(String value) => ProductId(value);

  @override
  String encode(ProductId value) => value.value;
}

@PathParameter.config(codec: ProductIdCodec)
final ProductId id;

A codec must implement DeepLinkValueCodec<T> and have a const unnamed, zero-argument constructor.

Validation

Use DeepLinkValidation on path, query, or fragment annotations:

@QueryParameter.config(
  name: 'coupon',
  validation: DeepLinkValidation(
    pattern: r'^[A-Z0-9]+$',
    minLength: 4,
    maxLength: 16,
    allowEmpty: false,
  ),
)
final String? coupon;

Numeric min/max, string or collection minLength/maxLength, regular expression pattern, and allowEmpty run both while parsing and building.

Named constructors

Set constructor when generated parsing should invoke a named constructor:

@DeepLink('/profiles/:name', constructor: 'fromUri')
final class ProfileLink {
  const ProfileLink.fromUri(this.name);
  final String name;
}

Errors and non-throwing parsing

parse distinguishes no match, bad values, rejected constructors, and build failures:

  • DeepLinkNotFoundException
  • DeepLinkFormatException
  • DeepLinkConstructionException
  • DeepLinkBuildException

Errors expose route, parameter, value, expectedType, and cause where relevant. Their toString() intentionally omits the full URI to avoid leaking sensitive query values into logs.

switch (AppLinks.parseResult(uri)) {
  case DeepLinkSuccess(:final value):
    handle(value);
  case DeepLinkFailure(:final error):
    report(error);
}

tryParse returns null only when no route matches; malformed matching routes still throw, preventing bad links from being mistaken for unrelated routes.

Manifest and router adapters

Every registry implements DeepLinkRouter<T> and exposes immutable metadata:

final router = AppLinks.router;
final routes = AppLinks.routes;

This is enough for custom routers, analytics allowlists, debug screens, and documentation tooling.

For Flutter Router:

import 'package:typed_deep_links_flutter/typed_deep_links_flutter.dart';

const parser = TypedDeepLinkRouteInformationParser<AppLink>(
  router: AppLinks.router,
);

For GoRouter, parse in a redirect or top-level builder and build locations from typed values:

AppLink? parseGoRouterState(GoRouterState state) =>
    AppLinks.tryParse(state.uri);

String locationFor(AppLink link) => AppLinks.toUri(link).toString();

For AutoRoute or Navigator, use the same AppLinks.parse result at the app boundary and switch on its typed value. No router package is imported by the runtime or generator.

Cross-library registries

An explicit registry can import and aggregate annotated route types from many libraries. Import route types without prefixes in the registry library, list them in routes, and choose a shared sealed/interface baseType. This keeps feature code separate while preserving one exhaustive app-level parser.

Complete Flutter example

The example demonstrates canonical and legacy paths, constraints, custom codecs, optional and catch-all segments, repeated queries, enum wire names, a typed registry, route manifest, Flutter adapter, and Navigator:

cd example
flutter pub get
dart run build_runner build
flutter run

Packages

  • typed_deep_links: annotations and dependency-light runtime.
  • typed_deep_links_generator: build-time generator.
  • typed_deep_links_flutter: optional Flutter Router API adapter.

Repository uses a Dart pub workspace. Contributors run flutter pub get once at repository root; all packages share one lockfile and package configuration.

License

MIT.

Libraries

Type-safe, router-agnostic deep links.