← Back to Blog

Fixing 'Invalid conversion from one format to another' (FormatException) in Dart

January 15, 202613 Minutes Read
flutterdartjsonparsingtroubleshootingerror handlingdata conversionmodelsapimobile development

Fixing "Invalid conversion from one format to another" (FormatException) in Dart

You're parsing JSON from an API, and suddenly your app crashes with:

FormatException: Invalid conversion from one format to another

Or more specifically:

FormatException: Invalid date format
FormatException: Unexpected character
FormatException: Invalid number format

These errors happen when Dart tries to convert data from one format to another, but the data doesn't match the expected format. This is especially common when working with JSON to Model conversions, date parsing, and number formatting.

Quick Solution: Always validate and handle parsing errors. Use try-catch blocks, provide default values, use nullable types, and validate data before parsing. For JSON, use jsonDecode() with error handling, and for models, implement safe fromJson() methods with null checks.

This guide covers the most common FormatException scenarios in Dart/Flutter and provides copy-paste-ready solutions for each.


Understanding FormatException

FormatException is thrown when:

  • String parsing fails - Can't convert string to number/date
  • JSON parsing fails - Invalid JSON structure
  • Number formatting fails - Invalid number format
  • Date parsing fails - Invalid date string format

The key to fixing these errors is defensive programming - always assume data might be invalid.


Scenario 1: JSON to Model Conversion (Most Common)

The Problem

Converting JSON to Dart models without proper error handling:

// ❌ Wrong - Will crash on invalid data
class User {
  final String name;
  final int age;
  final String email;

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        age = json['age'],
        email = json['email'];
}

// Usage
final user = User.fromJson(jsonDecode(response)); // Crashes if data is invalid

Solution 1: Safe fromJson with Null Checks

class User {
  final String name;
  final int age;
  final String email;

  User({
    required this.name,
    required this.age,
    required this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      name: json['name'] as String? ?? '',
      age: _parseInt(json['age']),
      email: json['email'] as String? ?? '',
    );
  }

  static int _parseInt(dynamic value) {
    if (value is int) return value;
    if (value is String) {
      return int.tryParse(value) ?? 0;
    }
    return 0;
  }

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'age': age,
      'email': email,
    };
  }
}

Solution 2: Using json_serializable (Recommended)

For production apps, use code generation:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final String name;
  final int age;
  final String email;

  User({
    required this.name,
    required this.age,
    required this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Setup:

# pubspec.yaml
dependencies:
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.7
  json_serializable: ^6.7.1

Generate code:

flutter pub run build_runner build

Solution 3: Safe JSON Parsing with Try-Catch

User? parseUser(String jsonString) {
  try {
    final json = jsonDecode(jsonString) as Map<String, dynamic>;
    return User.fromJson(json);
  } on FormatException catch (e) {
    print('JSON parsing error: $e');
    return null;
  } on TypeError catch (e) {
    print('Type conversion error: $e');
    return null;
  } catch (e) {
    print('Unexpected error: $e');
    return null;
  }
}

// Usage
final user = parseUser(apiResponse);
if (user != null) {
  // Use user
} else {
  // Handle error
}

Scenario 2: Date Parsing Errors

The Problem

Parsing date strings that might be in different formats or invalid:

// ❌ Wrong - Crashes on invalid date
final date = DateTime.parse('2024-01-15'); // Works
final date2 = DateTime.parse('invalid'); // FormatException!

Solution 1: Safe Date Parsing

DateTime? parseDate(String? dateString) {
  if (dateString == null || dateString.isEmpty) {
    return null;
  }

  try {
    return DateTime.parse(dateString);
  } on FormatException {
    // Try alternative formats
    try {
      return DateFormat('yyyy-MM-dd HH:mm:ss').parse(dateString);
    } on FormatException {
      try {
        return DateFormat('dd/MM/yyyy').parse(dateString);
      } on FormatException {
        print('Unable to parse date: $dateString');
        return null;
      }
    }
  }
}

// Usage
final date = parseDate(json['createdAt']);
if (date != null) {
  // Use date
}

Solution 2: Using Extension Methods

extension SafeDateTimeParsing on String {
  DateTime? toDateTimeOrNull() {
    try {
      return DateTime.parse(this);
    } on FormatException {
      return null;
    }
  }

  DateTime toDateTimeOrNow() {
    return toDateTimeOrNull() ?? DateTime.now();
  }
}

// Usage
final date = json['createdAt']?.toString().toDateTimeOrNull();
final dateWithDefault = json['createdAt']?.toString().toDateTimeOrNow();

Solution 3: Handling Multiple Date Formats

class DateParser {
  static final List<DateFormat> _formats = [
    DateFormat('yyyy-MM-dd'),
    DateFormat('yyyy-MM-dd HH:mm:ss'),
    DateFormat('yyyy-MM-ddTHH:mm:ssZ'),
    DateFormat('dd/MM/yyyy'),
    DateFormat('MM/dd/yyyy'),
    DateFormat('yyyy-MM-dd HH:mm:ss.SSS'),
  ];

  static DateTime? parse(String? dateString) {
    if (dateString == null || dateString.isEmpty) {
      return null;
    }

    for (final format in _formats) {
      try {
        return format.parse(dateString);
      } on FormatException {
        continue;
      }
    }

    // Last resort: try DateTime.parse
    try {
      return DateTime.parse(dateString);
    } on FormatException {
      return null;
    }
  }
}

// Usage
final date = DateParser.parse(json['createdAt']);

Scenario 3: Number Parsing Errors

The Problem

Converting strings to numbers when the string might not be a valid number:

// ❌ Wrong - Crashes on invalid number
final number = int.parse('123'); // Works
final number2 = int.parse('abc'); // FormatException!
final double = double.parse('12.34'); // Works
final double2 = double.parse('invalid'); // FormatException!

Solution 1: Using tryParse

// ✅ Safe parsing with defaults
final number = int.tryParse(json['age']?.toString() ?? '') ?? 0;
final price = double.tryParse(json['price']?.toString() ?? '') ?? 0.0;

// Or with custom default
final age = int.tryParse(json['age']?.toString() ?? '') ?? 18;

Solution 2: Extension Methods for Safe Parsing

extension SafeNumberParsing on String {
  int toIntOrZero() => int.tryParse(this) ?? 0;
  int? toIntOrNull() => int.tryParse(this);
  
  double toDoubleOrZero() => double.tryParse(this) ?? 0.0;
  double? toDoubleOrNull() => double.tryParse(this);
}

// Usage
final age = json['age']?.toString().toIntOrZero();
final price = json['price']?.toString().toDoubleOrNull();

Solution 3: Handling Dynamic Types

int parseToInt(dynamic value) {
  if (value is int) return value;
  if (value is double) return value.toInt();
  if (value is String) {
    return int.tryParse(value) ?? 0;
  }
  return 0;
}

double parseToDouble(dynamic value) {
  if (value is double) return value;
  if (value is int) return value.toDouble();
  if (value is String) {
    return double.tryParse(value) ?? 0.0;
  }
  return 0.0;
}

// Usage
final age = parseToInt(json['age']); // Handles int, double, or String
final price = parseToDouble(json['price']);

Scenario 4: JSON Decoding Errors

The Problem

Decoding invalid JSON strings:

// ❌ Wrong - Crashes on invalid JSON
final data = jsonDecode(response); // FormatException if JSON is malformed

Solution: Safe JSON Decoding

Map<String, dynamic>? safeJsonDecode(String? jsonString) {
  if (jsonString == null || jsonString.isEmpty) {
    return null;
  }

  try {
    final decoded = jsonDecode(jsonString);
    if (decoded is Map<String, dynamic>) {
      return decoded;
    } else if (decoded is List) {
      // Handle array response
      return {'data': decoded};
    }
    return null;
  } on FormatException catch (e) {
    print('Invalid JSON format: $e');
    print('JSON string: $jsonString');
    return null;
  } catch (e) {
    print('Unexpected error decoding JSON: $e');
    return null;
  }
}

// Usage
final data = safeJsonDecode(apiResponse);
if (data != null) {
  // Use data
}

Solution: Validating JSON Structure

class JsonValidator {
  static bool isValidJson(String jsonString) {
    try {
      jsonDecode(jsonString);
      return true;
    } catch (e) {
      return false;
    }
  }

  static Map<String, dynamic>? decodeWithValidation(String jsonString) {
    if (!isValidJson(jsonString)) {
      return null;
    }
    return safeJsonDecode(jsonString);
  }
}

Scenario 5: Nested JSON Parsing

The Problem

Accessing nested JSON properties that might not exist:

// ❌ Wrong - Crashes if structure is different
final city = json['address']['city']; // FormatException if address is null

Solution: Safe Nested Access

extension SafeJsonAccess on Map<String, dynamic> {
  T? getValue<T>(String key, [T? defaultValue]) {
    final value = this[key];
    if (value is T) return value;
    return defaultValue;
  }

  T? getNested<T>(String path, [T? defaultValue]) {
    final keys = path.split('.');
    dynamic current = this;

    for (final key in keys) {
      if (current is Map<String, dynamic>) {
        current = current[key];
        if (current == null) return defaultValue;
      } else {
        return defaultValue;
      }
    }

    if (current is T) return current;
    return defaultValue;
  }
}

// Usage
final city = json.getNested<String>('address.city', 'Unknown');
final zipCode = json.getNested<int>('address.zipCode', 0);

Solution: Using Optional Chaining Pattern

class SafeJson {
  final Map<String, dynamic> _json;

  SafeJson(this._json);

  SafeJson? operator [](String key) {
    final value = _json[key];
    if (value is Map<String, dynamic>) {
      return SafeJson(value);
    }
    return null;
  }

  T? get<T>(String key, [T? defaultValue]) {
    final value = _json[key];
    if (value is T) return value;
    return defaultValue;
  }

  T? getNested<T>(String path, [T? defaultValue]) {
    final keys = path.split('.');
    dynamic current = _json;

    for (final key in keys) {
      if (current is Map<String, dynamic>) {
        current = current[key];
        if (current == null) return defaultValue;
      } else {
        return defaultValue;
      }
    }

    if (current is T) return current;
    return defaultValue;
  }
}

// Usage
final safeJson = SafeJson(json);
final city = safeJson.getNested<String>('address.city', 'Unknown');

Scenario 6: API Response Parsing

The Problem

Parsing API responses that might have different structures or errors:

// ❌ Wrong - Assumes response is always valid
final response = await http.get(uri);
final data = jsonDecode(response.body);
final user = User.fromJson(data);

Solution: Robust API Response Parser

class ApiResponse<T> {
  final T? data;
  final String? error;
  final bool success;

  ApiResponse.success(this.data)
      : success = true,
        error = null;

  ApiResponse.error(this.error)
      : success = false,
        data = null;
}

class ApiParser {
  static ApiResponse<T> parseResponse<T>({
    required http.Response response,
    required T Function(Map<String, dynamic>) fromJson,
  }) {
    try {
      // Check HTTP status
      if (response.statusCode != 200) {
        return ApiResponse.error(
          'HTTP ${response.statusCode}: ${response.reasonPhrase}',
        );
      }

      // Parse JSON
      final json = safeJsonDecode(response.body);
      if (json == null) {
        return ApiResponse.error('Invalid JSON response');
      }

      // Check for error in response
      if (json.containsKey('error')) {
        return ApiResponse.error(json['error'] as String? ?? 'Unknown error');
      }

      // Parse data
      try {
        final data = fromJson(json);
        return ApiResponse.success(data);
      } catch (e) {
        return ApiResponse.error('Failed to parse response: $e');
      }
    } on FormatException catch (e) {
      return ApiResponse.error('Format error: $e');
    } catch (e) {
      return ApiResponse.error('Unexpected error: $e');
    }
  }

  static ApiResponse<List<T>> parseListResponse<T>({
    required http.Response response,
    required T Function(Map<String, dynamic>) fromJson,
  }) {
    try {
      if (response.statusCode != 200) {
        return ApiResponse.error(
          'HTTP ${response.statusCode}: ${response.reasonPhrase}',
        );
      }

      final json = safeJsonDecode(response.body);
      if (json == null) {
        return ApiResponse.error('Invalid JSON response');
      }

      // Handle array response
      List<dynamic>? items;
      if (json is List) {
        items = json;
      } else if (json.containsKey('data') && json['data'] is List) {
        items = json['data'] as List<dynamic>;
      } else if (json.containsKey('items') && json['items'] is List) {
        items = json['items'] as List<dynamic>;
      }

      if (items == null) {
        return ApiResponse.error('Response does not contain a list');
      }

      final parsedItems = items
          .map((item) {
            try {
              if (item is Map<String, dynamic>) {
                return fromJson(item);
              }
              return null;
            } catch (e) {
              print('Failed to parse item: $e');
              return null;
            }
          })
          .whereType<T>()
          .toList();

      return ApiResponse.success(parsedItems);
    } catch (e) {
      return ApiResponse.error('Unexpected error: $e');
    }
  }
}

// Usage
final response = await http.get(uri);
final result = ApiParser.parseResponse<User>(
  response: response,
  fromJson: (json) => User.fromJson(json),
);

if (result.success && result.data != null) {
  final user = result.data!;
  // Use user
} else {
  print('Error: ${result.error}');
}

Complete Safe Model Example

Here's a complete example with all safety measures:

import 'dart:convert';
import 'package:intl/intl.dart';

class Product {
  final String id;
  final String name;
  final double price;
  final int stock;
  final DateTime? createdAt;
  final List<String> tags;
  final Map<String, dynamic>? metadata;

  Product({
    required this.id,
    required this.name,
    required this.price,
    required this.stock,
    this.createdAt,
    this.tags = const [],
    this.metadata,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: _parseString(json['id'], ''),
      name: _parseString(json['name'], 'Unknown'),
      price: _parseDouble(json['price']),
      stock: _parseInt(json['stock']),
      createdAt: _parseDate(json['createdAt']),
      tags: _parseStringList(json['tags']),
      metadata: json['metadata'] as Map<String, dynamic>?,
    );
  }

  static String _parseString(dynamic value, String defaultValue) {
    if (value is String) return value;
    if (value != null) return value.toString();
    return defaultValue;
  }

  static int _parseInt(dynamic value) {
    if (value is int) return value;
    if (value is double) return value.toInt();
    if (value is String) return int.tryParse(value) ?? 0;
    return 0;
  }

  static double _parseDouble(dynamic value) {
    if (value is double) return value;
    if (value is int) return value.toDouble();
    if (value is String) return double.tryParse(value) ?? 0.0;
    return 0.0;
  }

  static DateTime? _parseDate(dynamic value) {
    if (value == null) return null;
    if (value is DateTime) return value;
    if (value is String) {
      try {
        return DateTime.parse(value);
      } catch (e) {
        // Try alternative formats
        final formats = [
          DateFormat('yyyy-MM-dd'),
          DateFormat('yyyy-MM-dd HH:mm:ss'),
        ];
        for (final format in formats) {
          try {
            return format.parse(value);
          } catch (e) {
            continue;
          }
        }
        return null;
      }
    }
    return null;
  }

  static List<String> _parseStringList(dynamic value) {
    if (value is List) {
      return value
          .map((item) => item?.toString() ?? '')
          .where((item) => item.isNotEmpty)
          .toList();
    }
    return [];
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'price': price,
      'stock': stock,
      'createdAt': createdAt?.toIso8601String(),
      'tags': tags,
      'metadata': metadata,
    };
  }

  static Product? fromJsonString(String jsonString) {
    try {
      final json = jsonDecode(jsonString) as Map<String, dynamic>;
      return Product.fromJson(json);
    } catch (e) {
      print('Failed to parse Product: $e');
      return null;
    }
  }
}

Best Practices

1. Always Use Nullable Types for Optional Fields

// ✅ Good
final DateTime? createdAt;
final String? description;

// ❌ Bad - Crashes if field is missing
final DateTime createdAt;
final String description;

2. Provide Default Values

// ✅ Good
final int age = json['age'] as int? ?? 0;
final String name = json['name'] as String? ?? 'Unknown';

// ❌ Bad - Crashes if null
final int age = json['age'] as int;

3. Validate Before Parsing

// ✅ Good
if (json.containsKey('age') && json['age'] != null) {
  final age = _parseInt(json['age']);
}

// ❌ Bad - Assumes field exists
final age = json['age'] as int;

4. Use Try-Catch for Critical Operations

// ✅ Good
try {
  final user = User.fromJson(json);
} on FormatException catch (e) {
  // Handle format error
} on TypeError catch (e) {
  // Handle type error
} catch (e) {
  // Handle unexpected error
}

5. Log Errors for Debugging

try {
  // Parse operation
} catch (e, stackTrace) {
  print('Error: $e');
  print('Stack trace: $stackTrace');
  // Log to crash reporting service
}

Testing Your Parsing Logic

Unit Test Example

void main() {
  test('Product.fromJson handles missing fields', () {
    final json = {'id': '1', 'name': 'Test'};
    final product = Product.fromJson(json);
    
    expect(product.id, '1');
    expect(product.name, 'Test');
    expect(product.price, 0.0); // Default value
    expect(product.stock, 0); // Default value
  });

  test('Product.fromJson handles invalid number', () {
    final json = {
      'id': '1',
      'name': 'Test',
      'price': 'invalid',
      'stock': 'not-a-number',
    };
    final product = Product.fromJson(json);
    
    expect(product.price, 0.0); // Should default to 0
    expect(product.stock, 0); // Should default to 0
  });

  test('Product.fromJson handles invalid date', () {
    final json = {
      'id': '1',
      'name': 'Test',
      'createdAt': 'invalid-date',
    };
    final product = Product.fromJson(json);
    
    expect(product.createdAt, isNull);
  });
}

Common Error Messages and Fixes

Error: "FormatException: Invalid date format"

Fix: Use safe date parsing with multiple format support (see Scenario 2).

Error: "FormatException: Unexpected character"

Fix: Validate JSON before parsing, use safeJsonDecode().

Error: "FormatException: Invalid number format"

Fix: Use tryParse() instead of parse(), provide default values.

Error: "TypeError: null is not a subtype of type 'String'"

Fix: Use nullable types and null-aware operators (??, ?.).

Error: "NoSuchMethodError: The method '[]' was called on null"

Fix: Check for null before accessing nested properties.


Conclusion

FormatException errors in Dart are preventable with defensive programming. Always:

  1. Validate data before parsing
  2. Use nullable types for optional fields
  3. Provide default values for missing data
  4. Handle errors with try-catch blocks
  5. Test edge cases with unit tests

Remember: Never trust external data. Always validate and handle errors gracefully.


Next Steps

  • Set up JSON schema validation for API responses
  • Implement comprehensive error logging
  • Add unit tests for all parsing logic
  • Consider using code generation (json_serializable) for complex models

Updated for Flutter 3.24+ and Dart 3.5+