Free & open source — no account required

v1.2.5-PRICING-19
Web & Frontend • Engineering Documentation

JSON to Dart Generator

This technical guide provides an in-depth analysis of the json to dart class engine, best practices for implementation, and data security standards.

JSON to Dart Class: Freezed, json_serializable, null Safety, and Flutter Widget Integration

Dart's sound null safety (introduced in Dart 2.12) means JSON-to-Dart conversion requires explicit decisions about every nullable field: String? vs String, int? vs int. The professional approach uses json_serializable for fromJson/toJson code generation and freezed for immutable data classes with copyWith, pattern matching with when(), and deep equality without boilerplate. This guide covers both tools, plus direct integration with Flutter's widget state management using freezed union types for loading/success/error UI states.

Live Example: json_serializable + freezed

// Input JSON (API response)
{
  "id": "prod_abc123",
  "name": "Wireless Keyboard",
  "price_cents": 7999,
  "description": null,
  "category": { "id": "cat_electronics", "name": "Electronics" },
  "images": ["https://cdn.example.com/kb1.jpg", "https://cdn.example.com/kb2.jpg"],
  "in_stock": true,
  "rating": 4.7,
  "created_at": "2024-03-15T10:00:00Z"
}

// pubspec.yaml dependencies:
// dependencies:
//   freezed_annotation: ^2.4.1
//   json_annotation: ^4.8.1
// dev_dependencies:
//   freezed: ^2.4.5
//   json_serializable: ^6.7.1
//   build_runner: ^2.4.7

// product.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'product.freezed.dart';   // generated by freezed
part 'product.g.dart';         // generated by json_serializable

@freezed
class Product with _$Product {
  const factory Product({
    required String id,
    required String name,
    @JsonKey(name: 'price_cents') required int priceCents,
    String? description,
    required Category category,
    required List<String> images,
    @JsonKey(name: 'in_stock') required bool inStock,
    required double rating,
    @JsonKey(name: 'created_at') required DateTime createdAt,
  }) = _Product;

  factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
}

@freezed
class Category with _$Category {
  const factory Category({
    required String id,
    required String name,
  }) = _Category;

  factory Category.fromJson(Map<String, dynamic> json) => _$CategoryFromJson(json);
}

// Generate code:
// dart run build_runner build --delete-conflicting-outputs

// Usage:
final product = Product.fromJson(jsonDecode(responseBody));
print(product.name);         // "Wireless Keyboard"
print(product.priceCents);   // 7999
print(product.category.name); // "Electronics"

// copyWith — create modified copies without mutation
final discounted = product.copyWith(priceCents: 5999);
print(discounted.name);       // "Wireless Keyboard" (unchanged)
print(discounted.priceCents); // 5999

// Deep equality — works out of the box with freezed
print(product == discounted); // false
print(product == product.copyWith()); // true

// toJson — serialize back to Map
final json = product.toJson();

Freezed Union Types for UI States

// Freezed's union/sealed class pattern for loading/success/error UI states
// product_state.dart

@freezed
class ProductState with _$ProductState {
  const factory ProductState.initial() = ProductInitial;
  const factory ProductState.loading() = ProductLoading;
  const factory ProductState.success({ required List<Product> products }) = ProductSuccess;
  const factory ProductState.error({ required String message, int? statusCode }) = ProductError;

  factory ProductState.fromJson(Map<String, dynamic> json) => _$ProductStateFromJson(json);
}

// Pattern matching in Flutter widget:
Widget build(BuildContext context) {
  return state.when(
    initial: () => const SizedBox.shrink(),
    loading: () => const CircularProgressIndicator(),
    success: (products) => ListView.builder(
      itemCount: products.length,
      itemBuilder: (ctx, i) => ProductCard(product: products[i]),
    ),
    error: (message, statusCode) => ErrorWidget(
      message: message,
      code:    statusCode,
      onRetry: () => context.read<ProductCubit>().loadProducts(),
    ),
  );
  // 'when' is exhaustive — compiler error if you miss a variant
}

// maybeWhen — handle some cases, use 'orElse' for the rest
state.maybeWhen(
  loading: () => showLoadingOverlay(),
  orElse:  () => hideLoadingOverlay(),
);

Null Safety: Required vs Optional Fields

// Dart null safety — explicit nullable vs non-nullable
// json_serializable handles both cases correctly

@JsonSerializable()
class UserProfile {
  // Non-nullable — must be present in JSON, throws if missing
  final String id;
  final String username;

  // Nullable — may be null or absent in JSON
  final String? bio;         // null if not provided
  final String? avatarUrl;

  // Non-nullable with default — uses default if absent in JSON
  final bool isVerified;
  final int followerCount;
  final DateTime createdAt;

  const UserProfile({
    required this.id,
    required this.username,
    this.bio,
    this.avatarUrl,
    this.isVerified = false,  // default value
    this.followerCount = 0,
    required this.createdAt,
  });

  factory UserProfile.fromJson(Map<String, dynamic> json) =>
      _$UserProfileFromJson(json);

  Map<String, dynamic> toJson() => _$UserProfileToJson(this);
}

// @JsonKey customization
@JsonSerializable()
class Order {
  @JsonKey(name: 'order_id')          // map snake_case JSON to camelCase Dart
  final String orderId;

  @JsonKey(fromJson: _parseDate)      // custom converter
  final DateTime placedAt;

  @JsonKey(includeIfNull: false)      // don't include null fields in toJson()
  final String? couponCode;

  @JsonKey(ignore: true)              // never serialize this field
  final bool isSelected;              // UI-only state

  static DateTime _parseDate(String s) => DateTime.parse(s);
  ...
}

Manual fromJson Without Code Generation

// For simple classes without freezed/json_serializable overhead:
class Address {
  final String street;
  final String city;
  final String country;
  final String? postalCode;

  const Address({
    required this.street,
    required this.city,
    required this.country,
    this.postalCode,
  });

  factory Address.fromJson(Map<String, dynamic> json) => Address(
    street:     json['street'] as String,
    city:       json['city']   as String,
    country:    json['country'] as String,
    postalCode: json['postal_code'] as String?,
  );

  Map<String, dynamic> toJson() => {
    'street':  street,
    'city':    city,
    'country': country,
    if (postalCode != null) 'postal_code': postalCode,  // conditional inclusion
  };
}

// Handling nested arrays of objects:
class Order {
  final String id;
  final List<OrderItem> items;

  factory Order.fromJson(Map<String, dynamic> json) => Order(
    id:    json['id'] as String,
    items: (json['items'] as List<dynamic>)
        .map((e) => OrderItem.fromJson(e as Map<String, dynamic>))
        .toList(),
  );
}

Best Practices for Production

  • Use freezed for shared data model classes: Freezed provides copyWith, structural equality, toString, and pattern matching with no boilerplate. Without it, you'd implement all of these manually for every model class — tedious and error-prone for teams managing 20+ models.
  • Use @JsonKey(name: 'snake_case_key') for API responses: Dart style is camelCase but REST APIs typically return snake_case. Using @JsonKey keeps your Dart code idiomatic while matching the wire format exactly. Don't rename JSON keys in the response mapping layer — map them here in the model.
  • Run build_runner in watch mode during development: dart run build_runner watch re-generates .g.dart and .freezed.dart files automatically when you change a model. Without watch mode, you'll frequently forget to regenerate and get confusing compile errors about missing _$ProductFromJson functions.
  • Never serialize UI state fields: Use @JsonKey(ignore: true) on any field that represents UI state (isSelected, isExpanded, errorMessage). These should not appear in API payloads or cached JSON. Keeping them in the same class is fine for flutter_bloc/Riverpod patterns, but mark them explicitly as non-serializable.

FAQ

Q: When should I use Freezed vs plain json_serializable?
A: Use Freezed when you need copyWith or sealed union types (common in Bloc/Riverpod state management). For simple DTOs that are only deserialized from API responses and never modified, plain json_serializable with const constructor is lighter and generates less code. Freezed adds value when immutability and exhaustive pattern matching matter.

Q: How do I handle dynamic/unknown JSON fields in Dart?
A: Use Map<String, dynamic> for fields that can hold arbitrary JSON objects, and Object? or dynamic for truly unknown values. Avoid using dynamic widely — it defeats null safety. For JSON with variable structure, consider using the json_path package for safe path-based access.

Q: How do I use a generated Dart class in Flutter with FutureBuilder?
A: FutureBuilder<Product>(future: fetchProduct(id), builder: (ctx, snap) { if (snap.hasData) return ProductCard(product: snap.data!); ... }). With Riverpod, use AsyncValue<Product> which gives you when(data:, error:, loading:) matching automatically — similar to Freezed unions but for async state.

Developer FAQ

Is the processing local-only?

Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.

Can I use this for enterprise projects?

Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.