api_contract 0.2.1
api_contract: ^0.2.1 copied to clipboard
Runtime API response contract validator for Flutter/Dart. Detect API mismatches before they crash your app. Works with any HTTP client.
api_contract #
A runtime API response contract validator for Flutter/Dart. Detect mismatches between your expected API contracts and actual server responses before they silently break your app.
๐ฏ The Problem #
Backend APIs change. Fields get renamed, types shift from int to String, optional fields become null, and new fields appear without warning. Your app keeps running โ but with wrong data, broken UI, or silent failures.
api_contract catches these mismatches at runtime by validating API responses against a contract you define.
โจ Features #
- โ Runtime validation - Catch API contract violations before they cause bugs
- โ Multiple contract modes - Strict or lenient validation
- โ Type checking - Validate field types (string, number, boolean, list, map)
- โ Nested objects - Support for complex nested structures
- โ Built-in types - DateTime, Uri, Duration auto-detected
- โ Code generation - Auto-generate contracts from model classes
- โ Repository pattern - Clean integration with repository layer
- โ CI/CD friendly - Throw errors in CI, log warnings in development
- โ Zero runtime overhead in release - Disable validation in production
๐ฆ Installation #
Add to your pubspec.yaml:
dependencies:
api_contract: ^0.2.1
dev_dependencies:
api_contract_generator: ^0.2.0
build_runner: ^2.4.13
Then run:
dart pub get
๐ Quick Start #
Option 1: Auto-Generate from Model (Recommended) #
1. Annotate your model:
import 'package:api_contract/api_contract.dart';
import 'package:api_contract_generator/api_contract_generator.dart';
part 'user.g.dart';
@ApiContractSchema(mode: ContractMode.strict, version: '1.0')
class User {
User({
required this.id,
required this.name,
required this.email,
this.avatar,
required this.createdAt,
});
final int id;
final String name;
final String email;
@optional
final String? avatar; // Can be missing from response
final DateTime createdAt; // Auto-detected as string in JSON
}
2. Generate contract:
dart run build_runner build --delete-conflicting-outputs
3. Use in repository:
class UserRepo {
Future<ApiResult<User>> getUser(int userId) async {
try {
final response = await _apiService.getUser(userId);
// Validate response
final validationResult = userContract.validate(response.toJson());
validationResult.throwIfInvalid();
return ApiResult.success(response);
} catch (error) {
return ApiResult.failure(error);
}
}
}
Option 2: Manual Contract Definition #
import 'package:api_contract/api_contract.dart';
final userContract = ApiContract(
mode: ContractMode.strict,
version: '1.0',
fields: {
'id': ContractField.required(type: FieldType.number),
'name': ContractField.required(type: FieldType.string),
'email': ContractField.required(type: FieldType.string),
'avatar': ContractField.optional(type: FieldType.string),
'createdAt': ContractField.required(type: FieldType.string),
},
);
// Validate
final result = userContract.validate(responseJson);
result.throwIfInvalid();
๐ Usage Guide #
Contract Modes #
// Strict mode - flags unexpected fields as violations
@ApiContractSchema(mode: ContractMode.strict)
// Lenient mode - ignores extra fields (default)
@ApiContractSchema(mode: ContractMode.lenient)
Field Annotations #
class Post {
final String title; // Required by default
@optional
final String? description; // Can be missing from response
@nullable
final String? author; // Must be present but can be null
final DateTime createdAt; // Auto-detected as string in JSON
}
Difference:
@optional: Field can be missing from JSON โContractField.optional()@nullable: Field must be present but value can benullโContractField.nullable()
Supported Types #
| Dart Type | JSON Type | Auto-Detected |
|---|---|---|
String |
string | โ |
int, double, num |
number | โ |
bool |
boolean | โ |
List |
array | โ |
Map |
object | โ |
DateTime |
string | โ |
Uri |
string | โ |
Duration |
number | โ |
| Custom classes | nested | โ |
Repository Pattern Integration #
class LessonRepo {
final LessonApiService _apiService;
Future<ApiResult<Lesson>> getLesson(int id) async {
try {
// 1. Fetch from API
final response = await _apiService.getLesson(id);
// 2. Validate contract
final validationResult = lessonContract.validate(response.toJson());
// 3. Handle violations
validationResult.when(
valid: () => print('โ Valid response'),
invalid: (violations) {
for (final v in violations) {
print('โ ${v.fieldPath}: ${v.message}');
}
},
);
// 4. Throw if invalid (in CI mode)
validationResult.throwIfInvalid();
return ApiResult.success(response);
} catch (error) {
return ApiResult.failure(error);
}
}
}
Global Configuration #
void main() {
ApiContractConfig.setup(
onViolation: ViolationBehavior.throwInCI, // Throw in CI, log otherwise
enableInRelease: false, // Disable in production
logPrefix: '[Contract]',
);
runApp(MyApp());
}
ViolationBehavior options:
log- Print violations (default)warn- Print warningsthrowAlways- Always throw exceptionthrowInCI- Throw only whenCI=trueenv var is setsilent- Ignore violations
๐งช CI/CD Integration #
Set CI=true in your CI environment:
# GitHub Actions
env:
CI: true
Then configure:
ApiContractConfig.setup(
onViolation: ViolationBehavior.throwInCI,
);
Contract violations will fail your pipeline before broken assumptions reach production.
๐ API Reference #
ApiContract #
| Method | Description |
|---|---|
validate(Map<String, dynamic> json) |
Validates JSON against contract |
upgrade({version, added, removed}) |
Creates new contract version |
copyWith({fields, mode, version}) |
Copies with overrides |
ApiContract.fromJson(Map) |
Auto-generate from sample JSON |
ApiContract.fromJsonSchema(Map) |
Generate from JSON Schema |
ContractField #
| Constructor | Description |
|---|---|
ContractField.required(type:) |
Required field |
ContractField.optional(type:) |
Optional (can be missing) |
ContractField.nullable(type:) |
Required but can be null |
ContractField.nested(nestedContract:) |
Nested object |
ContractField.list({listItemContract:}) |
List field |
ContractField.deprecated(type:, message:) |
Deprecated field |
ViolationType #
| Type | Description |
|---|---|
missingRequiredField |
Required field is absent |
typeMismatch |
Field has wrong type |
unexpectedField |
Extra field in strict mode |
nullableViolation |
Non-nullable field is null |
deprecatedFieldUsed |
Deprecated field present |
๐ Examples #
Check the example directory for complete examples:
๐ Version Upgrades #
Track contract evolution:
final v1 = ApiContract(version: '1.0', fields: {...});
final v2 = v1.upgrade(
version: '2.0',
added: {'avatar': ContractField.optional(type: FieldType.string)},
removed: ['legacyId'],
);
๐ค Contributing #
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Submit a pull request
๐ License #
MIT License. See LICENSE for details.
๐ Related Packages #
- api_contract_generator - Code generator companion package
๐ก Tips #
- Use
ContractMode.strictduring development to catch all changes - Switch to
ContractMode.lenientif backend adds optional fields frequently - Enable
throwInCIto fail builds on contract violations - Disable validation in production builds for performance
- Use
@optionalfor fields that might be missing - Use
@nullablefor fields that are always present but can be null
โก Performance #
- Zero overhead in release builds (when
enableInRelease: false) - Minimal overhead in debug builds
- Efficient type checking and validation
- No reflection used
Made with โค๏ธ for Flutter developers tired of silent API breakages.