---
name: freezed-jsonkey-backend-field-mismatch
description: |
  Fix Freezed model JSON deserialization crashes caused by backend sending different field
  names than Dart property names. Use when: (1) "type 'Null' is not a subtype of type 'String'
  in type cast" in generated .g.dart fromJson, (2) model works in tests but crashes with live
  API data, (3) backend uses snake_case or short names while Dart uses camelCase or descriptive
  names (e.g., backend sends "name" but Dart expects "userName"). Covers @JsonKey annotation,
  test fixture updates, and systematic audit approach.
author: Claude Code
version: 1.0.0
date: 2026-02-19
---

# Freezed @JsonKey Backend Field Name Mismatch

## Problem

Freezed-generated `fromJson` uses exact property names as JSON keys by default. When the
backend sends different field names (e.g., `name` instead of `userName`), the generated code
does `json['userName'] as String` which returns `null` (key doesn't exist) and crashes on
the strict `as String` cast.

## Context / Trigger Conditions

- **Error message**: `_TypeError: type 'Null' is not a subtype of type 'String' in type cast`
- **Stack trace points to**: `_$ModelNameFromJson` in a `.g.dart` file
- **Tests pass but live API fails**: Test fixtures use Dart property names, not actual API names
- **Common mismatch patterns**:
  - Backend `name` → Dart `userName`
  - Backend `avatar` → Dart `avatarUrl`
  - Backend `created_at` → Dart `createdAt` (snake_case)
  - Any enrichment/transformation in backend that renames fields

## Solution

### Step 1: Identify the Mismatch

1. Read the actual API response (from debug logs or network inspector)
2. Read the generated `.g.dart` file to see what keys the parser expects
3. Compare: `json['expectedKey']` vs actual JSON key in response

### Step 2: Add @JsonKey Annotations

```dart
@freezed
abstract class MyModel with _$MyModel {
  const factory MyModel({
    required int userId,
    /// Backend sends `name`, not `userName`
    @JsonKey(name: 'name') required String userName,
    /// Backend sends `avatar`, not `avatarUrl`
    @JsonKey(name: 'avatar') String? avatarUrl,
  }) = _MyModel;

  factory MyModel.fromJson(Map<String, dynamic> json) =>
      _$MyModelFromJson(json);
}
```

### Step 3: Regenerate Code

```bash
# In the package directory containing the model
dart run build_runner build --delete-conflicting-outputs
```

### Step 4: Update Test Fixtures

Tests that create JSON maps must use the **API field names** (not Dart property names):

```dart
// WRONG - uses Dart property names
final json = {'userId': 1, 'userName': 'John', 'avatarUrl': 'http://...'};

// CORRECT - uses actual API field names
final json = {'userId': 1, 'name': 'John', 'avatar': 'http://...'};
```

### Step 5: Audit Related Models

When one model has mismatches, check ALL models that parse the same backend data:
- Models sharing the same entity type (e.g., `ChannelMemberModel`, `ChannelMemberDetailedModel`)
- Response wrapper models that contain the affected model
- Any model from the same API endpoint group

## Verification

1. Regenerated `.g.dart` shows correct key: `userName: json['name'] as String,`
2. All unit tests pass with updated fixtures
3. Live API data parses without crashes
4. For dependency packages: full stop + `flutter run` (not just hot restart)

## Example

**Before** (crashes with live data):
```dart
const factory ChannelMemberDetailedModel({
  required int userId,
  required String userName,      // Generated: json['userName'] as String → null → CRASH
  String? avatarUrl,             // Generated: json['avatarUrl'] as String? → always null
}) = _ChannelMemberDetailedModel;
```

**After** (works with live data):
```dart
const factory ChannelMemberDetailedModel({
  required int userId,
  @JsonKey(name: 'name') required String userName,    // json['name'] as String
  @JsonKey(name: 'avatar') String? avatarUrl,         // json['avatar'] as String?
}) = _ChannelMemberDetailedModel;
```

## Debugging Approach

When you see a `_TypeError` in a `.g.dart` `fromJson`:

1. **Add a catch-all in your repository guard** to capture the exact error:
   ```dart
   } catch (e, stackTrace) {
     print('[Repo] Unexpected error: ${e.runtimeType}: $e');
     print('[Repo] Stack trace: $stackTrace');
     throw WrappedException(message: 'Failed to process response', originalError: e);
   }
   ```

2. **Read the actual API response** from debug output or network tools

3. **Compare field names** between API JSON and generated `.g.dart` parser

4. **Apply @JsonKey** annotations for all mismatched fields

5. **Regenerate + update tests + verify**

## Notes

- `@JsonKey(name: 'api_field')` affects BOTH `fromJson` and `toJson` — the model will
  serialize with the API field name too, which is usually what you want
- For nullable fields, the mismatch doesn't crash (returns null) but data is silently lost
- PHP `enrichMembersWithUserInfo()` and similar enrichment functions are common sources
  of renamed fields — always check the actual backend code, not just the schema
- When fixing a dependency package (like `buyerkiosk_chat`), changes require a full
  stop + `flutter run`, not just hot restart

## References

- [Freezed @JsonKey documentation](https://pub.dev/packages/freezed#fromjson---tojson)
- [json_serializable @JsonKey](https://pub.dev/packages/json_serializable#supported-types)
