JSON to Dart Model Generator

Paste a JSON response from your API and get null-safe Dart classes with fromJson and toJson, ready for Flutter or any Dart project. Add several responses from the same endpoint and the generator merges them, so fields that are sometimes missing or null become nullable. Everything runs on your device; your JSON is never uploaded.

Runs in your browser · Your JSON is never uploaded

Loading the tool…

Tip: press Ctrl + Enter (Cmd + Enter on a Mac) to generate. Use Add response to merge several API responses.

How to use the JSON to Dart Model Generator

  1. Paste a JSON response into Response 1, or open a .json file from your device.
  2. Optional: add more responses from the same endpoint, such as a second page, an error-free response for a different user, or a response where optional fields are missing.
  3. Enter the root class name, for example UserProfile.
  4. Select Generate (Ctrl + Enter, or Cmd + Enter on a Mac).
  5. Review the notes that explain which fields became nullable and why, then copy the code or save it as a .dart file.

How types and names are chosen

Nullable fields
A field is nullable when it is null in any response or missing from any object at that position, including objects inside arrays. Each nullable field is listed in the notes with the responses that caused it.
Numbers
Numbers written without a decimal point or exponent become int. If a field has both whole and decimal numbers, it becomes double, and fromJson converts with toDouble(), so 980 and 1250.5 both parse.
Mixed and unknown types
A field whose values have different JSON types (for example a string in one response and a number in another) becomes dynamic. A field that is only ever null also becomes dynamic. An array that is empty in every response becomes List<dynamic>. Each fallback is explained in the notes.
Original keys
fromJson and toJson always use the exact key from your JSON, including spaces, symbols and capital letters. Only the Dart field name changes: user_id becomes userId.
Names and collisions
Keys that are Dart keywords get a Value suffix (class → classValue). Keys starting with a digit get a field prefix (2fa → field2fa). If two keys map to the same name, the second gets a number (userId2). Nested classes are named after their key, in singular form for arrays (items → Item, categories → Category). If a class name is already used or is a dart:core type such as List, the parent class name is added in front.
Top-level arrays
If your response is an array of objects, the generator creates the item class plus helper functions such as productListFromJson and productListToJson.

Examples

These outputs were produced by the same code that runs in the tool above.

Single response: customer profile

One response from a customer endpoint with a nested object, a list of strings, a list of orders and a null value. The order totals mix whole and decimal numbers, so they become double, and one order has an extra coupon field, so coupon becomes nullable. Root class name: CustomerProfile.

Input JSON

{
  "id": 1024,
  "full_name": "Amina Rahman",
  "email": "amina@example.com",
  "is_verified": true,
  "rating": 4.8,
  "avatar_url": null,
  "address": {
    "line1": "Office 1204, Bay Square",
    "city": "Dubai",
    "country_code": "AE"
  },
  "tags": ["wholesale", "priority"],
  "recent_orders": [
    {
      "order_id": "SO-55120",
      "total": 1250.5,
      "item_count": 3,
      "placed_at": "2026-10-28T09:15:00Z"
    },
    {
      "order_id": "SO-55187",
      "total": 980,
      "item_count": 2,
      "placed_at": "2026-10-30T14:02:00Z",
      "coupon": "AUTUMN10"
    }
  ]
}

Generated Dart (customer_profile.dart)

// Generated by Zvaad Tools: JSON to Dart Model Generator.
// https://tools.zvaad.com/json-to-dart/

class CustomerProfile {
  const CustomerProfile({
    required this.id,
    required this.fullName,
    required this.email,
    required this.isVerified,
    required this.rating,
    this.avatarUrl,
    required this.address,
    required this.tags,
    required this.recentOrders,
  });

  factory CustomerProfile.fromJson(Map<String, dynamic> json) {
    return CustomerProfile(
      id: (json['id'] as num).toInt(),
      fullName: json['full_name'] as String,
      email: json['email'] as String,
      isVerified: json['is_verified'] as bool,
      rating: (json['rating'] as num).toDouble(),
      avatarUrl: json['avatar_url'],
      address: Address.fromJson(json['address'] as Map<String, dynamic>),
      tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
      recentOrders: (json['recent_orders'] as List<dynamic>).map((e) => RecentOrder.fromJson(e as Map<String, dynamic>)).toList(),
    );
  }

  final int id;
  final String fullName;
  final String email;
  final bool isVerified;
  final double rating;
  final dynamic avatarUrl;
  final Address address;
  final List<String> tags;
  final List<RecentOrder> recentOrders;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'id': id,
      'full_name': fullName,
      'email': email,
      'is_verified': isVerified,
      'rating': rating,
      'avatar_url': avatarUrl,
      'address': address.toJson(),
      'tags': tags,
      'recent_orders': recentOrders.map((e) => e.toJson()).toList(),
    };
  }
}

class Address {
  const Address({
    required this.line1,
    required this.city,
    required this.countryCode,
  });

  factory Address.fromJson(Map<String, dynamic> json) {
    return Address(
      line1: json['line1'] as String,
      city: json['city'] as String,
      countryCode: json['country_code'] as String,
    );
  }

  final String line1;
  final String city;
  final String countryCode;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'line1': line1,
      'city': city,
      'country_code': countryCode,
    };
  }
}

class RecentOrder {
  const RecentOrder({
    required this.orderId,
    required this.total,
    required this.itemCount,
    required this.placedAt,
    this.coupon,
  });

  factory RecentOrder.fromJson(Map<String, dynamic> json) {
    return RecentOrder(
      orderId: json['order_id'] as String,
      total: (json['total'] as num).toDouble(),
      itemCount: (json['item_count'] as num).toInt(),
      placedAt: json['placed_at'] as String,
      coupon: json['coupon'] as String?,
    );
  }

  final String orderId;
  final double total;
  final int itemCount;
  final String placedAt;
  final String? coupon;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'order_id': orderId,
      'total': total,
      'item_count': itemCount,
      'placed_at': placedAt,
      'coupon': coupon,
    };
  }
}
  • Type fallback · avatar_url: Only null values were found, so the type is dynamic. Add a sample with a real value to get a specific type.
  • Type fallback · recent_orders[].total: Both integers and decimal numbers were found, so the type is double. Integers are converted with toDouble().
  • Nullable · recent_orders[].coupon: Nullable because the field is missing from 1 of 2 objects (response 1).

Multiple responses: paginated products

Two pages from the same product-list endpoint. Page 2 has no next page (null), one product has no discount at all, another has a discount object, and only some variants include "limited". Combining both pages gives one model that accepts every response. Root class name: ProductPage.

Input JSON 1

{
  "page": 1,
  "per_page": 2,
  "total": 3,
  "next_page": 2,
  "data": [
    {
      "sku": "ZV-1001",
      "name": "Ceramic Mug",
      "price": 24.0,
      "stock": 120,
      "variants": [{ "color": "White", "sku": "ZV-1001-W" }],
      "discount": null
    },
    {
      "sku": "ZV-1002",
      "name": "Glass Bottle",
      "price": 39.5,
      "stock": 0,
      "variants": [],
      "discount": { "type": "percent", "value": 10 }
    }
  ]
}

Input JSON 2

{
  "page": 2,
  "per_page": 2,
  "total": 3,
  "next_page": null,
  "data": [
    {
      "sku": "ZV-1003",
      "name": "Steel Tumbler",
      "price": 55,
      "stock": 42,
      "variants": [
        { "color": "Black", "sku": "ZV-1003-B", "limited": true }
      ]
    }
  ]
}

Generated Dart (product_page.dart)

// Generated by Zvaad Tools: JSON to Dart Model Generator.
// https://tools.zvaad.com/json-to-dart/

class ProductPage {
  const ProductPage({
    required this.page,
    required this.perPage,
    required this.total,
    this.nextPage,
    required this.data,
  });

  factory ProductPage.fromJson(Map<String, dynamic> json) {
    return ProductPage(
      page: (json['page'] as num).toInt(),
      perPage: (json['per_page'] as num).toInt(),
      total: (json['total'] as num).toInt(),
      nextPage: (json['next_page'] as num?)?.toInt(),
      data: (json['data'] as List<dynamic>).map((e) => DataItem.fromJson(e as Map<String, dynamic>)).toList(),
    );
  }

  final int page;
  final int perPage;
  final int total;
  final int? nextPage;
  final List<DataItem> data;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'page': page,
      'per_page': perPage,
      'total': total,
      'next_page': nextPage,
      'data': data.map((e) => e.toJson()).toList(),
    };
  }
}

class DataItem {
  const DataItem({
    required this.sku,
    required this.name,
    required this.price,
    required this.stock,
    required this.variants,
    this.discount,
  });

  factory DataItem.fromJson(Map<String, dynamic> json) {
    return DataItem(
      sku: json['sku'] as String,
      name: json['name'] as String,
      price: (json['price'] as num).toDouble(),
      stock: (json['stock'] as num).toInt(),
      variants: (json['variants'] as List<dynamic>).map((e) => Variant.fromJson(e as Map<String, dynamic>)).toList(),
      discount: json['discount'] == null ? null : Discount.fromJson(json['discount'] as Map<String, dynamic>),
    );
  }

  final String sku;
  final String name;
  final double price;
  final int stock;
  final List<Variant> variants;
  final Discount? discount;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'sku': sku,
      'name': name,
      'price': price,
      'stock': stock,
      'variants': variants.map((e) => e.toJson()).toList(),
      'discount': discount?.toJson(),
    };
  }
}

class Variant {
  const Variant({
    required this.color,
    required this.sku,
    this.limited,
  });

  factory Variant.fromJson(Map<String, dynamic> json) {
    return Variant(
      color: json['color'] as String,
      sku: json['sku'] as String,
      limited: json['limited'] as bool?,
    );
  }

  final String color;
  final String sku;
  final bool? limited;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'color': color,
      'sku': sku,
      'limited': limited,
    };
  }
}

class Discount {
  const Discount({
    required this.typeValue,
    required this.value,
  });

  factory Discount.fromJson(Map<String, dynamic> json) {
    return Discount(
      typeValue: json['type'] as String,
      value: (json['value'] as num).toInt(),
    );
  }

  final String typeValue;
  final int value;

  Map<String, dynamic> toJson() {
    return <String, dynamic>{
      'type': typeValue,
      'value': value,
    };
  }
}
  • Nullable · next_page: Nullable because the field is null in response 2.
  • Type fallback · data[].price: Both integers and decimal numbers were found, so the type is double. Integers are converted with toDouble().
  • Nullable · data[].variants[].limited: Nullable because the field is missing in response 1.
  • Renamed · data[].discount.type: The Dart field is named typeValue because "type" is a Dart keyword. The original key is still used for JSON.
  • Nullable · data[].discount: Nullable because the field is missing from 1 of 3 objects (response 2) and null in response 1.

Frequently asked questions

Is my JSON uploaded anywhere?

No. Parsing, type inference and code generation run on your device. Your input and the generated code are not sent over the network, not added to URLs or analytics, and not stored. A file is only written when you choose Save, Download or Share.

Why should I paste more than one response?

One response only shows the fields that happened to be present. Pasting several responses from the same endpoint lets the generator see which fields can be missing or null, so the model does not crash on the next response.

Does the generated code need json_serializable or build_runner?

No. The classes use plain Dart and only need dart:convert to decode the response, for example: UserProfile.fromJson(jsonDecode(body) as Map<String, dynamic>).

Why is a field dynamic instead of a specific type?

Because the responses gave conflicting or no type information: different JSON types for the same key, only null values, or an array that was always empty. Add a response with a real value and generate again, or change the type by hand.

Does toJson recreate my original JSON exactly?

It uses the original keys, but nullable fields that were missing are written as null, and numbers in double fields are written as decimals. Key order follows the first time each key was seen.

Are dates converted to DateTime?

No. Date strings stay String so parsing never fails on an unexpected format. Convert them with DateTime.parse where you use them.

Limitations and edge cases

  • All responses must have the same top-level shape: either all objects or all arrays of objects.
  • Objects with the same structure at different paths get separate classes; they are not deduplicated.
  • Dates, enums and maps with dynamic keys are generated as String or nested classes; adjust them by hand if needed.
  • Very large inputs (several megabytes) can make the text editor slow. A few representative items per array are enough for accurate models. The limit is 20 MB in total.

Read the privacy policy for how the site handles data.