LogoSkills

Data Mapper Pattern

Rules for writing Mapper classes that convert OpenAPI Response to Domain Entity.

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#

FeatureFile PathMain 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#

ItemPatternExample
File name {feature}_mapper.dart classroom_mapper.dart
Class name{Feature}MapperClassroomMapper
Conversion method from{ResponseType}() fromClassResponse()
Parsing method parse{EnumType}() parseWithdrawReason()
Error mappingmapException()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#

TypeExample
Credentialspassword, userPw, pin
TokensaccessToken, refreshToken, idToken
IdentificationuserId, loginId (exclude for logging purposes)
Personal InfophoneNumber, email
Auth CodessmsCode, 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 CodeFailure TypeDescriptionExample
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
409ConflictFailureConflictDuplicate data
500+ServerFailureServer errorInternal 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}Mapper in 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.parse prohibited)
  • 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#

PurposePrefixExample
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#

  1. Consistency: All error logging follows the same structure
  2. Parsable: Log aggregation tools can parse error field
  3. Information preservation: stackTrace stored as structured field