smart_snake 0.5.1 copy "smart_snake: ^0.5.1" to clipboard
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 version pub points pub likes license GitHub stars

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) and smartSnakeUserFromJsonString(source)
  • ClassNameParam for typed request payloads from @SmartField(withRequest: true)
  • request body helpers such as toBodyRequest(), toRequestBody(), toRequestJson() and multipart helpers
  • generated entities, toEntity(), and entity copyWith() when enableEntity: true is 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 annotation
  • FieldRename.snake support by default
  • FieldRename.none, pascal, kebab, and screamingSnake support through json_annotation
  • @JsonKey(name: '...') and @SmartField(overwriteName: '...') key overrides
  • nullable and non-nullable fields
  • safer parsing for bool, int, double, num, and DateTime
  • 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 entity copyWith() with enableEntity: true
  • custom field converters through fromJson, toJson, and toEntity expressions
  • automatic enum .name mapping 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 example json['last_login_at'].
  • {field} is the model field name, for example lastLoginAt.

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, or payload is 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.
1
likes
150
points
15
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Dart/Flutter code generator for clean architecture models, request params, generated entities, and JSON mapping.

Repository (GitHub)
View/report issues

Topics

#codegen #json #clean-architecture #model-mapper #build-runner

License

MIT (license)

Dependencies

analyzer, build, json_annotation, source_gen

More

Packages that depend on smart_snake