Hello everyone. I have developed a command‐line tool using TypeScript that offers the following features:
- Generates class code from
JSON
. - The generated classes support both serialization and deserialization.
- Works with all valid
JSON
data formats. - Supports
JSON
as well asJSON5
. - Uses the
ref
syntax to reference predefined structures, enabling the creation of recursive types. - Currently, supports the
dart
andarkTs
languages, with plans to support others in the future.
Tool Links
Why Develop This Tool
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.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.
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
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,
},
}
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;
}
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
}
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' } }
},
}
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());
}
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"
}
}
}
main() {
final result = testresult();
result.fromPreset();
setState(() {});
}
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:
- The field exists in both the input and the definition, and the types match perfectly. This is the ideal scenario—simply assign the value.
- The field exists in both, but the types do not match. In this case, the
DiffType
enumeration determines how to fill the value. - 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:
- 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 ) |
- 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
Top comments (0)