smart_snake 0.5.1
smart_snake: ^0.5.1 copied to clipboard
A Dart/Flutter code generator for clean architecture models, request params, generated entities, and JSON mapping.
🐍 smart_snake
Generate clean Dart models, request params, entities, and JSON mappers with less boilerplate.
pub.dev · GitHub · Issues & feedback · Usage
smart_snake is a Dart/Flutter code generator built for Clean Architecture projects.
It helps you keep your model layer small by generating repetitive mapping code automatically.
Current release: 0.5.1 — a pre-1.0 stable release intended for real project testing before the public API is finalized as 1.0.0.
What it generates #
toJson()via extension- private
_$ClassNameFromJson(Map<String, dynamic>)internals - private
_$ClassNameFromJsonString(String)internals - public helpers such as
smartSnakeUserFromJson(json)andsmartSnakeUserFromJsonString(source) ClassNameParamfor typed request payloads from@SmartField(withRequest: true)- request body helpers such as
toBodyRequest(),toRequestBody(),toRequestJson()and multipart helpers - generated entities,
toEntity(), and entitycopyWith()whenenableEntity: trueis used
The package focuses on reducing repetitive model/request/entity mapping code while keeping your data source, repository and response-wrapper logic in your own project.
Features #
@SmartSnake()class annotationFieldRename.snakesupport by defaultFieldRename.none,pascal,kebab, andscreamingSnakesupport throughjson_annotation@JsonKey(name: '...')and@SmartField(overwriteName: '...')key overrides- nullable and non-nullable fields
- safer parsing for
bool,int,double,num, andDateTime - nested model parsing through generated public helpers such as
smartSnakeTokenModelFromJson(...) - request conditions with raw Dart expressions
- request-only fields through
@SmartField(requestOnly: true) - multipart file separation from normal body maps
- optional generated entity classes,
toEntity()mapping, and entitycopyWith()withenableEntity: true - custom field converters through
fromJson,toJson, andtoEntityexpressions - automatic enum
.namemapping for enum fields Map<String, CustomModel>parsing/serialization/entity mapping
Installation #
dependencies:
smart_snake: ^0.5.1
dev_dependencies:
build_runner: ^2.11.1
Usage #
import 'package:smart_snake/smart_snake.dart';
part 'user.g.dart';
@SmartSnake()
class User {
final String firstName;
final String lastName;
final int age;
final String? email;
const User({
required this.firstName,
required this.lastName,
required this.age,
this.email,
});
}
Generate code:
dart run build_runner build --delete-conflicting-outputs
Then use:
final user = User(firstName: 'John', lastName: 'Doe', age: 30);
final map = user.toJson();
final jane = smartSnakeUserFromJsonString(
'{"first_name":"Jane","last_name":"Smith","age":25}',
);
Optional Model.fromJson(...) syntax #
Dart generators cannot inject a new constructor or factory into your original class. If you want this exact API:
User.fromJson(json);
User.fromJsonString(source);
add these two one-line factories manually:
factory User.fromJson(Map<String, dynamic> json) => smartSnakeUserFromJson(json);
factory User.fromJsonString(String source) => smartSnakeUserFromJsonString(source);
If you do not care about the User.fromJson(...) syntax, you can skip them and use the generated public helpers directly.
SmartField options #
SmartField is optional. Use it only when a field needs behavior different from the class default.
| Option | Meaning | Typical use |
|---|---|---|
withRequest |
Includes the field in the generated *Param class and request body helpers. |
Login/register request fields. |
skipIfNull |
Omits the field from request maps when the value is null. |
Nullable values without isEmpty, such as int?, bool?, File?. |
skipIfEmpty |
Omits empty String, List, Set, Iterable, or Map values. Also skips null for nullable fields. |
Optional strings, lists, maps. |
condition |
Raw Dart expression emitted inside the generated Param class. | condition: 'codeLogin == false' with skipIfEmpty: true. |
overwriteName |
Overrides the generated JSON/request key. | API uses verifyCode while class naming is Pascal/snake. |
fromJson |
Custom expression/function for parsing this field from JSON. | Normalize email, parse custom date/enum values. |
toJson |
Custom expression/function for writing this field to JSON. | Format dates, serialize custom values. |
toEntity |
Custom expression/function for mapping this model field to entity. | Mask email, convert model value into a domain value object. |
entityFieldName |
Changes only the named argument used in generated toEntity(). |
Model field email maps to entity field maskedEmail. |
entityTypeName |
Overrides the generated entity field type. | CityModel generates/customizes LocationEntity. |
ignoreEntity |
Excludes the field from generated entity class and toEntity(). |
Debug/internal response-only data. |
requestOnly |
Keeps the field only in generated request params and excludes it from response/entity mapping. | Files, passwords, confirm passwords, device ids. |
isFile |
Marks the request field as multipart file input. File fields are excluded from normal body maps. | File, List<File>, upload inputs. |
Current constructor:
const SmartField({
this.withRequest = false,
this.skipIfNull = false,
this.skipIfEmpty = false,
this.condition,
this.overwriteName,
this.fromJson,
this.toJson,
this.toEntity,
this.entityFieldName,
this.entityTypeName,
this.ignoreEntity = false,
this.requestOnly = false,
this.isFile = false,
});
Removed API: paramOnly and justParam. Use requestOnly instead.
Entity generation and mapping #
If you want smart_snake to generate the domain entity class for a model, enable entity generation on that model:
@SmartSnake(
fieldRename: FieldRename.snake,
enableEntity: true,
)
class LoginModel {
final TokenModel? token;
const LoginModel({this.token});
}
@SmartSnake(enableEntity: true)
class TokenModel {
final String accessToken;
const TokenModel({required this.accessToken});
}
The generated code includes entity classes and mappers:
class LoginEntity {
final TokenEntity? token;
const LoginEntity({this.token});
LoginEntity copyWith({Object? token = _smartSnakeLoginModelCopyWithUnset}) {
return LoginEntity(
token: identical(token, _smartSnakeLoginModelCopyWithUnset)
? this.token
: token as TokenEntity?,
);
}
}
extension SmartSnakeLoginModelEntityExtension on LoginModel {
LoginEntity toEntity() {
return LoginEntity(
token: token?.toEntity(),
);
}
}
Nested entity classes are generated per annotated class. In practice, annotate each model that should have a generated entity with enableEntity: true; this avoids surprising recursive generation across files.
You can customize the generated entity class name:
@SmartSnake(enableEntity: true, entityName: 'LoginDomainEntity')
class LoginModel {}
entityName is only a generated class-name override. It does not point to a previously created entity and it does not generate anything unless enableEntity is also true.
If the entity constructor parameter should have a different name, override it per field:
@SmartField(entityFieldName: 'data')
final TokenDataModel dataToken;
If a response field should not be included in the generated entity or toEntity() mapping:
@SmartField(ignoreEntity: true)
final String? debugMessage;
Generated entities always use named constructor parameters.
Generated entities also include copyWith(). Nullable fields can be explicitly set to null because the generated method uses an internal sentinel value instead of treating null as “not provided”:
final updated = entity.copyWith(
fullName: 'Updated name',
cityModel: null,
);
Request params #
Mark only request/body fields with @SmartField(withRequest: true):
@SmartSnake(fieldRename: FieldRename.none)
class PostLoginModel {
@SmartField(withRequest: true)
final String username;
@SmartField(
withRequest: true,
condition: 'codeLogin == false',
skipIfEmpty: true,
)
final String? password;
@SmartField(
withRequest: true,
condition: 'codeLogin == true',
skipIfEmpty: true,
)
final String? verifyCode;
final bool codeLogin;
PostLoginModel({
required this.username,
this.password,
this.verifyCode,
this.codeLogin = false,
});
}
The generated param can be used in API providers:
final body = PostLoginModelParam(
username: username,
password: password,
codeLogin: false,
).toRequestBody();
condition is emitted as a raw Dart expression inside the generated *Param class. Use field names directly, not param.fieldName or model.fieldName. Prefer combining simple conditions with skipIfEmpty/skipIfNull instead of writing long null checks. If you need to check a nullable value inside condition, write null-aware Dart such as verifyCode?.isNotEmpty == true.
skipIfNull and skipIfEmpty #
@SmartField(withRequest: true, skipIfNull: true)
final String? optionalValue;
@SmartField(withRequest: true, skipIfEmpty: true)
final String? search;
skipIfEmpty supports String, List, Iterable, Set, and Map. For nullable fields it also skips null, so skipIfNull is not needed for those types. Keep skipIfNull for nullable values that do not support isEmpty, such as int?, bool?, DateTime?, File?, or custom objects.
Request-only fields #
requestOnly excludes a field from response/entity mapping while keeping it available in generated request params. Use it for values that are sent to the server but are not part of the response model, such as files, passwords, device ids, and confirm passwords.
@SmartField(
withRequest: true,
overwriteName: 'avatar',
requestOnly: true,
isFile: true,
)
final File? avatar;
File fields are excluded from normal body maps and emitted only by toMultipartFiles().
final fields = param.toMultipartFields();
final files = param.toMultipartFiles();
final multipart = param.toMultipartBody();
smart_snake does not depend on Dio, http, or any other network client. This keeps the package flexible. toMultipartBody() returns a generic SmartSnakeMultipartBody:
final multipart = param.toMultipartBody();
// plain fields
multipart.fields;
// file values, for example File or List<File>
multipart.files;
// merged fields + files, useful for custom clients
multipart.toMap();
For Dio, convert the generated maps in your data source layer:
final multipart = param.toMultipartBody();
final formData = FormData.fromMap({
...multipart.fields,
for (final entry in multipart.files.entries)
entry.key: await MultipartFile.fromFile((entry.value as File).path),
});
For package:http, use multipart.fields for text fields and convert multipart.files to MultipartFile values.
Custom converters #
Use converters when a field needs project-specific parsing or serialization.
DateTime? parseServerDate(dynamic value) {
if (value == null) return null;
return DateTime.parse(value as String).toLocal();
}
@SmartField(
fromJson: 'parseServerDate({value})',
toJson: '{field}?.toIso8601String()',
)
final DateTime? lastLoginAt;
Placeholders:
{value}is the raw JSON value, for examplejson['last_login_at'].{field}is the model field name, for examplelastLoginAt.
If no placeholder is used, smart_snake treats the string as a function name:
@SmartField(fromJson: 'parseUserRole', toJson: 'roleToJson')
final UserRole role;
Generated usage becomes approximately:
role: parseUserRole(json['role']);
map['role'] = roleToJson(role);
Enum support #
Enums are still supported, but there is no isEnum option anymore.
smart_snake detects Dart enum fields automatically when the API value is the enum name:
enum UserRole { guest, admin, operator }
class UserModel {
final UserRole role;
const UserModel({required this.role});
}
Generated parsing uses the enum name:
UserRole.values.byName(json['role'] as String)
Generated serialization uses:
role.name
So this works for values like:
{ "role": "admin" }
If your API uses custom enum wire values, do not use a special enum flag.
Use fromJson and toJson instead:
@SmartField(
fromJson: 'UserRoleX.fromWire({value})',
toJson: 'UserRoleX.toWire({field})',
)
final UserRole role;
Nested maps #
Nested maps are supported for common API shapes:
final Map<String, PermissionModel> permissionsByModule;
The generated code maps values through generated model helpers and entity mappers:
Map<String, PermissionModel> -> Map<String, PermissionEntity>
Real clean-architecture test scenario #
The realistic flow is intentionally kept in test/ instead of example/, because this package is a generator and the important behavior should be verified automatically. The test structure mirrors a real project:
test/
models/
user_model.dart
register_model.dart
remote/
api.dart
user_datasource.dart
user_repository.dart
user_repository_flow_test.dart
main.dart
It covers:
- model parsing from a fake remote response
- repository mapping
response -> model -> generated entity - generated entity classes
- nested models
- list of nested models
Map<String, CustomModel>mapping- enum mapping
- safe parsing for bool/int/double from flexible API values
- request body conditions
skipIfEmpty- generic multipart/form-data payload separation
Run the tests after generating code:
dart pub get
dart run build_runner build --delete-conflicting-outputs
dart test test/user_repository_flow_test.dart
You can also run the manual smoke flow:
dart run test/main.dart
example/bin/example.dart is intentionally minimal. Real usage scenarios live in tests.
Questions, ideas, and feedback #
For bugs, feature requests, API design discussions, or unexpected generated output, please use GitHub Issues:
https://github.com/yasinowo/smart-snake/issues
Issues are easier to track than private messages and they help other users find the same answer later.
Notes #
- Response wrapper logic such as reading
data,result, orpayloadis intentionally not handled by this package. - Re-run build runner after changing annotations.
- If generated code fails, the generator should throw a focused error for common condition mistakes such as unknown field names.