DEV Community

yangfanzn
yangfanzn

Posted on

1

How to Generate Code for Flutter?

Hello everyone. I have developed a command‐line tool using TypeScript that offers the following features:

  1. Generates class code from JSON.
  2. The generated classes support both serialization and deserialization.
  3. Works with all valid JSON data formats.
  4. Supports JSON as well as JSON5.
  5. Uses the ref syntax to reference predefined structures, enabling the creation of recursive types.
  6. Currently, supports the dart and arkTs languages, with plans to support others in the future.

Tool Links

Why Develop This Tool

  1. When I first started developing with Flutter, I found the official json_serializable approach to be somewhat cumbersome, it required defining a lot of boilerplate and ensuring that properties matched specific conventions. When combined with HTTP requests, the amount of manual template code increases even further.

  2. In my view, naming types can be a painful process. Even if you are working on a project by yourself, it is challenging to maintain a consistent naming convention from start to finish. In a team setting, even basic camelCase naming might not be reliably enforced. Ultimately, the specific name of a class is not critical; what matters is that the naming is completely consistent and easily identifiable.

  3. In any strongly typed language, to ensure type safety when working with dynamic data, you must predefine class types and implement serialization/deserialization. Although different languages handle this in various ways, the underlying structure of classes and the logic for serialization/deserialization are fundamentally similar.

Examples

Below, I will demonstrate how to use this tool. As this is my first open source project, there might be aspects I haven’t fully considered yet—your feedback and suggestions are most welcome.

Getting Started

Since this is a command‐line tool, you start from the terminal. I’ll begin by using npx; other execution methods are similar. Detailed usage instructions can be found in the documentation at the Tool Links.

npx json2class build -l dart@3
Enter fullscreen mode Exit fullscreen mode

The -l flag is shorthand for --language and is used to specify the target language. Currently, it supports dart@3 and arkTs@12 with the number following @ representing the version.

When you run this command, it will, by default,search for all .json and .json5 files in the current directory (including subdirectories up to three levels deep).

If everything is correct, the tool will generate a code file in the current directory.For Dart, it will create json2class.dart; for arkTs, it will create json2class.ets.

You can specify a directory to search for .json or .json5 files
using the -s or --search flag (both absolute and relative paths are supported).

You can specify an output directory using the -o or --output flag.

It is recommended to add the generated file json2class.* to your git ignore list.

Configuring JSON

As long as your JSON is valid and constitutes an effective configuration,
code generation will work smoothly.

1. A Simple Example

// Filename: test.json5
{
  a: 1,
  b: "bbb",
  c: true,
  o: {
    o1: 100,
    o2: 'ooo',
    o3: false,
  },
}
Enter fullscreen mode Exit fullscreen mode

The generated Dart code will roughly look like this:

class test {
  num a = 0;
  String b = '';
  bool c = false;
  testo o = testo();
}

class testo {
  num o1 = 0;
  String o2 = '';
  bool o3 = false;
}
Enter fullscreen mode Exit fullscreen mode

Indeed, I use a combination of the filename and the property name to form the class name. This method not only ensures consistent naming throughout but also retains clear identifiable.

From the generated code, you can see the relationship between the property types/default values and the original JSON. If you prefer not to set default values, please refer to the documentation at the Tool Links for further details.

2. How to Create Recursive Types?

As mentioned earlier, while valid JSON can be processed without errors, it does not always guarantee that the desired type structure will be generated.

class test {
  num a = 0;
  String b = '';
  bool c = false;
  testo o = testo();
}
class testo {
  num o1 = 0;
  String o2 = '';
  bool o3 = false;
  testo? child; // Recursive type
}
Enter fullscreen mode Exit fullscreen mode

For a recursive type like the one above, plain JSON cannot fully describe it. We need to adopt a special syntax:

// test.json5
{
  a: 123,
  b: 'abc',
  c: true,
  o: {
    o1: 100,
    o2: 'ooo',
    o3: false,
    child: { $meta: { ref: '/test#/o' } }
  },
}
Enter fullscreen mode Exit fullscreen mode

The ref reference is split into two parts by the #:

/test is the filename. This can be any file within your JSON search directory,
even if the file resides in a subdirectory (in which case, include the folder hierarchy).

/o specifies the field whose type you wish to reference; in this example, it is testo.
If the referenced type is in the current file, you can omit the filename and simply write { ref: '#/o' }. (Note that the # is mandatory.)

Currently, under $meta, only the ref configuration is supported.
If you have suggestions for additional features, please let me know,
future updates might incorporate more options under $meta.

3. How to Handle Conflicts and Special Characters?

Since class names are generated by concatenating the filename and the property name, there may be extreme cases where type conflicts occur. In such instances, the command‐line tool will prompt you, and simply renaming the file will resolve the conflict.

Renaming the file results in a new class name. Once again, the exact name of the class isn’t critical—the important factors are:

  • The consistency of the class name generation rule.
  • The identifiable of the class names.

If a field contains special characters that are reserved keywords in the target language, the tool will automatically convert them using a specific algorithm. This conversion does not affect property usage or the serialized field names.

How to Use the Generated Code

1. Serialization and Deserialization

import 'json2class.dart';

main() {
  final t = testo();
  t.fromJson({
    "test": {
      "o1": 100,
      "o2": 'ooo',
      "o3": true,
     }
  });
  print(t.toJson());
}
Enter fullscreen mode Exit fullscreen mode

The core functionality of the generated code is serialization and deserialization:

  • fromJson: Deserializes JSON data.
  • toJson: Serializes the object to JSON.

For convenience in converting JSON data from a string format, a method named fromAny is also provided. It accepts any input, attempts to convert it into a Map, and then calls fromJson to perform deserialization.

For complete documentation on using the generated code, please refer to the Tool Links. We won’t elaborate further here.

2. Constructing Mock Data

It is foreseeable that the ideal use case for the generated code is when retrieving data from a network. When the backend is not yet ready for integration testing, you might need to construct mock data for development. Once you receive the API response schema from the backend, you can simultaneously configure mock data.

// test.json
{
  "result": {
     "statusCode": "",
     "statusMessage": "",
     "data": {
        "userName": "json2class",
        "userPhone": "13888888888",
        "userAvator": "http://xxx.yyy.com/avator.png"
     }
  }
}
Enter fullscreen mode Exit fullscreen mode
main() {
  final result = testresult();
  result.fromPreset();
  setState(() {});
}
Enter fullscreen mode Exit fullscreen mode

By using the fromPreset method, the data configured in the JSON is populated into the object, allowing you to proceed with business logic. When the backend is ready for integration, simply replace fromPreset with the actual API call. Of course, you could also encapsulate this further, using generics to provide a unified API interface and enabling one, click switching for mock data with a single parameter.

3. Filling Rules

Why were filling rules designed? In frontend backend interactions, to ensure robust code, no backend data should be blindly trusted.
This is why we deserialize and type-check the JSON data received from the backend.

In real world scenarios, if the API response structure or types do not match our definitions, what should be done? Should an error be thrown? That would not be acceptable. When there is a discrepancy in structure or type, filling in with null might work, or assigning a default value for that type might be preferable. To accommodate different needs, filling rules were designed.

  • For Object Keys

There are essentially three cases:

  1. The field exists in both the input and the definition, and the types match perfectly. This is the ideal scenario—simply assign the value.
  2. The field exists in both, but the types do not match. In this case, the DiffType enumeration determines how to fill the value.
  3. The field is missing in the input. Here, the MissKey enumeration is used to decide how to fill it.
  • For Arrays of Equal Length

This is straightforward: if the input array and the defined array have the same length, each element is filled based on its position.
If a type mismatch occurs at a particular position, it is handled according to DiffType.

  • For Arrays of Unequal Length

For extra elements when the array lengths differ, the following rules apply:

  1. Enumeration MoreIndex: When the input array length is greater than the defined array.
Enumeration Effect
Fill Inserts the input value; if there is a type mismatch, sets a default value or null based on whether the field is optional
Drop Discards the extra input data so that the array length matches the defined array
Null Fills the extra elements with null (for non-optional fields, Null behaves the same as Fill)
  1. Enumeration MissIndex: When the input array length is less than the defined array.
Enumeration Effect
Fill Fills with default values; for multidimensional arrays, the filling is performed recursively
Drop Discards the extra defined data so that the array length matches the input array
Null Fills the missing elements with null (for non-optional fields, Null behaves the same as Fill)
Skip Leaves the extra defined data unchanged

Typically, when receiving an array from the backend, you would instantiate a new object. In such cases, the array’s initial length is 0 (i.e., the input array length is greater than the defined array), so the behavior is governed by the MoreIndex enumeration. The default setting for MoreIndex is MoreIndex.Fill, meaning the input data is filled into the defined array element by element, which is the expected behavior.

Conclusion

That’s all the content. Thank you for reading. This project has evolved through years of practical use, iterative improvement, and refinement. I sincerely hope you find it useful in your projects.
If you encounter any issues or have suggestions for improvements,
please feel free to provide feedback through the following channels:

  • Submit issues or suggestions via GitHub Issues
  • Email Yang Fan<yangfanzn@gmail.com>
  • Leave a comment in the discussion section

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post