Rules for writing Mapper classes that convert OpenAPI Response to Domain Entity.
Directory Structure#
feature/{app_or_common}/{feature_name}/lib/src/
โโโ data/
โ โโโ mappers/ # Mapper ํด๋์ค ์์น
โ โ โโโ {feature}_mapper.dart
โ โโโ repository/
โ โ โโโ mixins/
โ โ โโโ {feature}_openapi_mixin.dart # Mapper ์ฌ์ฉ
โ โโโ data.dart # mappers export ํฌํจ
โโโ domain/
Current Mapper File List#
| Feature | File Path | Main Conversion |
|---|---|---|
| attendance | feature/application/attendance/.../attendance_mapper.dart |
QR verification, attendance records |
| classroom | feature/application/classroom/.../classroom_mapper.dart |
Class, student, folder info |
| homework | feature/application/homework/.../homework_mapper.dart |
Homework list, progress |
| league | feature/application/league/.../league_mapper.dart |
League, ranking info |
| level_test | feature/application/level_test/.../level_test_mapper.dart |
Level test, questions, results |
| notice_board | feature/application/notice_board/.../notice_board_mapper.dart |
Notice board, attachments |
| notification | feature/application/notification/.../notification_mapper.dart |
Push notifications |
| payment | feature/application/payment/.../payment_mapper.dart |
Payment info |
| report | feature/application/report/.../report_mapper.dart |
Learning reports |
| review | feature/application/review/.../review_mapper.dart |
Wrong answer notes |
| auth | feature/common/auth/.../auth_mapper.dart |
Authentication, tokens |
| mypage | feature/common/mypage/.../mypage_mapper.dart |
User profile |
| settings | feature/common/settings/.../settings_mapper.dart |
App settings |
Note: core.dart re-exports Mappers from all Feature packages, so all Mappers are accessible via just
import 'package:core/core.dart';
Mapper Class Structure#
import 'package:core/core.dart';
import 'package:dependencies/dependencies.dart';
import 'package:openapi/api.dart';
/// Mapper for converting {Feature} API Response to Domain Entity
abstract final class {Feature}Mapper {
/// Converts {Response} to {Entity}
static {Entity} from{Response}({Response} response) {
return {Entity}(
// ํ๋ ๋งคํ
);
}
/// Converts API error to Failure
static Failure mapException(Object error, StackTrace stackTrace) {
Log.e('{Feature} API Error', error: error, stackTrace: stackTrace);
if (error is DioException) {
return NetworkFailure(
error.message ?? 'Network error occurred',
error: error,
stackTrace: stackTrace,
);
}
return UnexpectedFailure(
error.toString(),
error: error is Exception ? error : null,
stackTrace: stackTrace,
);
}
}
Rules#
Class Declaration#
- Use
abstract final class(prevent instantiation) - All methods declared as
static
Naming Conventions#
| Item | Pattern | Example |
|---|---|---|
| File name | {feature}_mapper.dart |
classroom_mapper.dart |
| Class name | {Feature}Mapper | ClassroomMapper |
| Conversion method | from{ResponseType}() |
fromClassResponse() |
| Parsing method | parse{EnumType}() |
parseWithdrawReason() |
| Error mapping | mapException() | Common usage |
Writing Conversion Methods#
// Good: Clear null handling with defaults
static ClassroomClassInfo fromClassResponse(ClassResponse response) {
return ClassroomClassInfo(
classId: response.classId?.toString() ?? '',
name: response.name ?? '',
createdAt: response.createdAt ?? DateTime.now(),
);
}
// Bad: Direct usage without null check
static ClassroomClassInfo fromClassResponse(ClassResponse response) {
return ClassroomClassInfo(
classId: response.classId.toString(), // NPE ์ํ
name: response.name,
);
}
Safe Parsing Patterns#
int.tryParse usage (required)
// โ
Good: Safe parsing with int.tryParse
static int parseUserId(String? value) {
return int.tryParse(value ?? '') ?? 0;
}
// โ Bad: Direct int.parse usage (risk of exception)
static int parseUserId(String? value) {
return int.parse(value!); // FormatException ์ํ
}
Enum conversion with parseXXX methods
/// Converts String to WithdrawReason enum
static WithdrawReason parseWithdrawReason(String? value) {
return switch (value) {
'INCONVENIENT' => WithdrawReason.inconvenient,
'CONTENT_UNSATISFIED' => WithdrawReason.contentUnsatisfied,
'COST_BURDEN' => WithdrawReason.costBurden,
'LOW_FREQUENCY' => WithdrawReason.lowFrequency,
'OTHER_SERVICE' => WithdrawReason.otherService,
'ETC' => WithdrawReason.etc,
_ => WithdrawReason.etc, // Default value
};
}
OpenAPI Enum Pattern#
Enum types generated from OpenAPI schema use @JsonEnum annotation.
Required Implementation Pattern#
When using @JsonEnum(valueField: 'json'), toJson() method must be added manually.
// package/openapi/lib/src/api/models/{enum_name}.dart
@JsonEnum(valueField: 'json')
enum StudentEnrollmentItemType {
create('CREATE'),
existing('EXISTING');
const StudentEnrollmentItemType(this.json);
final String json;
// โ
Required: toJson() method (called by build_runner in .g.dart)
String toJson() => json;
// โ
Recommended: fromJson() factory method
static StudentEnrollmentItemType fromJson(String value) {
return StudentEnrollmentItemType.values.firstWhere(
(item) => item.json == value,
orElse: () => StudentEnrollmentItemType.existing, // Safe default
);
}
}
When Error Occurs#
Error: The method ' toJson ' isn ' t defined for the type ' EnumType ' .
' type ' : instance.type.toJson(),
Solution: Add String toJson() => json; method to the enum file
Checklist#
- Verify
@JsonEnum(valueField: 'json')annotation final String json;field exists- Add
String toJson() => json;method - Add
static fromJson()method (with default value)
Usage in Repository Mixin#
import 'package:{feature}/src/data/mappers/{feature}_mapper.dart';
mixin {Feature}OpenApiMixin implements I{Feature}Repository {
@override
Future<Either<Failure, {Entity}>> get{Entity}ById(String id) async {
try {
final response = await openApiService.{feature}Api.get{Entity}(id: id);
return Right({Feature}Mapper.from{Response}(response));
} on Exception catch (error, stackTrace) {
return Left({Feature}Mapper.mapException(error, stackTrace));
}
}
}
Export Configuration#
Add mapper export to data/data.dart:
export 'mappers/{feature}_mapper.dart';
export 'repository/{feature}_repository.dart';
export 'repository/factories/factories.dart';
Security Rules (Logging)#
Sensitive Information Logging Prohibited#
// โ Prohibited: Including sensitive info in logs
Log.d('๐ ๋ก๊ทธ์ธ ์๋: userId=$userId');
Log.d('๐ฑ SMS ์ธ์ฆ: phoneNumber=$phoneNumber, code=$code');
Log.d('๐ ํ ํฐ ๊ฐฑ์ : refreshToken=$refreshToken');
// โ
Recommended: Log status only, exclude sensitive info
Log.d('๐ ๋ก๊ทธ์ธ API ํธ์ถ');
Log.d('๐ฑ SMS ์ธ์ฆ๋ฒํธ ํ์ธ API ํธ์ถ');
Log.d('๐ ํ ํฐ ๊ฐฑ์ API ํธ์ถ');
Sensitive Information List#
| Type | Example |
|---|---|
| Credentials | password, userPw, pin |
| Tokens | accessToken, refreshToken, idToken |
| Identification | userId, loginId (exclude for logging purposes) |
| Personal Info | phoneNumber, email |
| Auth Codes | smsCode, verificationCode, authCode |
DioException Handling#
Import Rules#
// โ
Correct: Import through dependencies package
import 'package:dependencies/dependencies.dart';
// DioException, DioExceptionType ์ฌ์ฉ ๊ฐ๋ฅ
// โ Prohibited: Direct dio package import
import 'package:dio/dio.dart';
Error Handling Pattern#
} on DioException catch (error, stackTrace) {
Log.e('โ API ์๋ฌ', error: error, stackTrace: stackTrace);
// Branch by HTTP status code (recommended)
if (error.response?.statusCode == 401) {
return const Left(AuthFailure.tokenExpired);
}
return Left(_mapException(error, stackTrace));
} on Exception catch (error, stackTrace) {
Log.e('โ API ์๋ฌ', error: error, stackTrace: stackTrace);
return Left(_mapException(error, stackTrace));
}
HTTP Status Code to Failure Mapping#
| Status Code | Failure Type | Description | Example |
|---|---|---|---|
| 400 | ValidationFailure |
Bad request | Missing required parameter |
| 401 | AuthFailure.tokenExpired |
Auth expired | Token expired/invalid |
| 403 | AuthFailure.forbidden |
Forbidden | Insufficient access |
| 404 | NotFoundFailure |
Resource not found | Non-existent data |
| 409 | ConflictFailure | Conflict | Duplicate data |
| 500+ | ServerFailure | Server error | Internal server error |
์ํ ์ฝ๋๋ณ ๋ถ๊ธฐ ์when
static Failure mapException(Object error, StackTrace stackTrace) {
if (error is DioException) {
final statusCode = error.response?.statusCode;
return switch (statusCode) {
400 => ValidationFailure(error.message ?? '์๋ชป๋ ์์ฒญ'),
401 => const AuthFailure.tokenExpired(),
403 => const AuthFailure.forbidden(),
404 => NotFoundFailure(error.message ?? '๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์'),
409 => ConflictFailure(error.message ?? '๋ฐ์ดํฐ ์ถฉ๋'),
>= 500 => ServerFailure(error.message ?? '์๋ฒ ์ค๋ฅ'),
_ => NetworkFailure(error.message ?? 'Network error'),
};
}
return UnexpectedFailure(error.toString(), stackTrace: stackTrace);
}
Checklist#
Default Configuration#
- Create mapper file in
data/mappers/directory - Use
abstract final class - Declare all methods as
static - Add export to
data.dart - Use
{Feature}Mapperin Repository mixin
Naming Patterns#
- Name conversion methods as
from{Response}() - Name enum parsing methods as
parse{EnumType}()
Safe Code#
- Null-safe field mapping (provide defaults)
- Use
int.tryParse(int.parseprohibited) - Enum conversion with switch expression (include default)
- No sensitive info in logs (userId, password, token, etc.)
Error Handling#
- Include
mapException()common error handling - Return appropriate Failure per HTTP status code
- 401 -> AuthFailure.tokenExpired
- 404 -> NotFoundFailure
- 500+ -> ServerFailure
- Include both error and stackTrace parameters in Log.e() calls
Private Helper Method Pattern#
Naming Rules#
| Purpose | Prefix | Example |
|---|---|---|
| Enum/Status conversion | _map* |
_mapProgressStatus(), _mapTemplate() |
| Field/data extraction | _extract* |
_extractChoices(), _extractStimulus() |
| Conditional calculation | None | _calculateAccuracyRate(), _validateData() |
Usage Examples#
// โ
CORRECT
static HomeworkAssignmentStatus _mapProgressStatus(ProgressStatus? status) {
return switch (status) {
ProgressStatus.notStarted => HomeworkAssignmentStatus.notStarted,
ProgressStatus.inProgress => HomeworkAssignmentStatus.inProgress,
ProgressStatus.completed => HomeworkAssignmentStatus.completed,
_ => HomeworkAssignmentStatus.notStarted,
};
}
static List<QuizChoice> _extractChoices(ResponseItem item) {
return item.choices?.map((c) => QuizChoice(
text: c.text ?? '',
isCorrect: c.isCorrect ?? false,
)).toList() ?? [];
}
// โ WRONG
static HomeworkAssignmentStatus _progressStatusMapper(...) { } // ๋์ฌ ๋จผ์
static List<QuizChoice> _getChoices(...) { } // _get์ ํฌํผ ๋ฉ์๋์ ๋ถ์ ์
Log.e() Structured Pattern (Required)#
Standard Pattern#
// โ
CORRECT: error์ stackTrace ๋ชจ๋ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌ
Log.e('Homework API Error', error: error, stackTrace: stackTrace);
Log.e('โ API ์๋ฌ', error: error, stackTrace: stackTrace);
// โ WRONG: ๋ฌธ์์ด ๋ณด๊ฐ ์ฌ์ฉ (ํ์ฑ ๋ถ๊ฐ)
Log.e('API Error: $error', stackTrace: stackTrace);
Log.e('API Error: $error');
// โ WRONG: stackTrace ๋๋ฝ
Log.e('API Error', error: error);
Reason#
- Consistency: All error logging follows the same structure
- Parsable: Log aggregation tools can parse
errorfield - Information preservation: stackTrace stored as structured field