├── lib ├── json_to_dart.dart ├── warning.dart ├── model_generator.dart ├── helpers.dart └── syntax.dart ├── .vscode ├── settings.json └── launch.json ├── test_resources ├── matrix.json ├── test_missing.json ├── double.json ├── test_warnings.json ├── bug_39.json ├── bug_10.json ├── array_root.json ├── test.json └── bug_40.json ├── .gitignore ├── pubspec.yaml ├── test ├── array_root_test.dart ├── matrix_test.dart ├── bug_39_test.dart ├── bug_40_test.dart ├── model_generator_warnings_test.dart ├── helpers_test.dart ├── double_test.dart ├── generated │ ├── sample.dart │ ├── bug_ten.dart │ └── sample_private.dart ├── bug_10_test.dart ├── model_generator_test.dart └── model_generator_private_test.dart ├── .travis.yml ├── example ├── sample.json ├── generate_model_example.dart └── generate_model_private_example.dart ├── README.md └── LICENSE /lib/json_to_dart.dart: -------------------------------------------------------------------------------- 1 | export './model_generator.dart'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /test_resources/matrix.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": [[{ "b": "c" }]] 3 | } 4 | -------------------------------------------------------------------------------- /lib/warning.dart: -------------------------------------------------------------------------------- 1 | class Warning { 2 | final String warning; 3 | final String path; 4 | 5 | Warning(this.warning, this.path); 6 | } 7 | 8 | class WithWarning { 9 | final T result; 10 | final List warnings; 11 | 12 | WithWarning(this.result, this.warnings); 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files created by dart2js. 4 | *.dart.js 5 | *.js_ 6 | *.js.deps 7 | *.js.map 8 | 9 | 10 | # Files and directories created by pub 11 | .buildlog 12 | .dart_tool/ 13 | .packages 14 | .pub/ 15 | build/ 16 | pubspec.lock 17 | 18 | # Directory created by dartdoc 19 | doc/api/ -------------------------------------------------------------------------------- /test_resources/test_missing.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "javiercbk", 3 | "favouriteInteger": null, 4 | "favouriteDouble": 1.6180, 5 | "url": "https://api.github.com/users/javiercbk", 6 | "tags": ["dart", "json", "cool"], 7 | "randomIntegers": [1, 2, 3], 8 | "randomDoubles": [1.1, 2.2, 3.3], 9 | "personalInfo": { 10 | "firstName": "Javier", 11 | "lastName": null 12 | } 13 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: json_to_dart 2 | version: 1.0.5 3 | description: A library that generates Dart classes (parse and generator included) from a json string. 4 | homepage: https://github.com/javiercbk/json_to_dart 5 | environment: 6 | sdk: ">=2.12.0 <3.0.0" 7 | dependencies: 8 | json_ast: "^1.0.6" 9 | convert: "^3.0.1" 10 | dart_style: "^2.2.1" 11 | 12 | dev_dependencies: 13 | test: "^1.20.1" 14 | path: "^1.8.1" 15 | -------------------------------------------------------------------------------- /test_resources/double.json: -------------------------------------------------------------------------------- 1 | { 2 | "int1": 1, 3 | "double2": 1e0, 4 | "double3": 1e1, 5 | "double4": 1.1e1, 6 | "double5": 10e-1, 7 | "double6": 1000.000e-1, 8 | "double7": 1000.0e-1, 9 | "double8": 1000.0e-0, 10 | "double9": 10.0000e-1, 11 | "double10": 10.000000000000000000000000001, 12 | "double11": 10.0, 13 | "double12": 10.1e0, 14 | "double13": 10.01e1, 15 | "double14": 11e-1, 16 | "double15": 11.0000e-1, 17 | "double16": 0.1 18 | } -------------------------------------------------------------------------------- /test_resources/test_warnings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambiguousArray": ["str", 12], 3 | "ambiguous": [{ 4 | "shouldBeDouble": 1, 5 | "arr": [] 6 | }, { 7 | "shouldBeDouble": 1.0, 8 | "arr": [{ 9 | "emptyArr": [] 10 | }, { 11 | "amb": 123 12 | }] 13 | }, { 14 | "shouldBeDouble": 1.1, 15 | "arr": [{ 16 | "str": "randomStr", 17 | "amb": "123" 18 | }] 19 | }], 20 | "emptyArr": [] 21 | } -------------------------------------------------------------------------------- /test_resources/bug_39.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": 0, 3 | "data": { 4 | "statistic1": { 5 | "count": [[2]], 6 | "duration": [[7200]], 7 | "x": ["经销商门店1"], 8 | "y": ["DREAM-6"] 9 | }, 10 | "statistic2": { 11 | "count": [6], 12 | "duration": [21600], 13 | "x": ["经销商门店1"] 14 | }, 15 | "statistic3": { 16 | "count": [3], 17 | "duration": [10800], 18 | "x": ["DREAM-6"] 19 | }, 20 | "statistic4": { 21 | "count": [2], 22 | "duration": [7200], 23 | "x": ["88888887"] 24 | } 25 | }, 26 | "message": "" 27 | } 28 | -------------------------------------------------------------------------------- /test_resources/bug_10.json: -------------------------------------------------------------------------------- 1 | { 2 | "glossary": { 3 | "title": "example glossary", 4 | "GlossDiv": { 5 | "title": "S", 6 | "GlossList": { 7 | "GlossEntry": { 8 | "ID": "SGML", 9 | "SortAs": "SGML", 10 | "GlossTerm": "Standard Generalized Markup Language", 11 | "Acronym": "SGML", 12 | "Abbrev": "ISO 8879:1986", 13 | "GlossDef": { 14 | "para": "A meta-markup language, used to create markup languages such as DocBook.", 15 | "GlossSeeAlso": ["GML", "XML"] 16 | }, 17 | "GlossSee": "markup" 18 | } 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /test/array_root_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:json_to_dart/json_to_dart.dart' show ModelGenerator; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group("model-generator", () { 8 | test("Should generate the classes to parse the JSON", () { 9 | final jsonRawData = 10 | new File("test_resources/array_root.json").readAsStringSync(); 11 | final generator = ModelGenerator('ArrayRoot'); 12 | final dartCode = generator.generateDartClasses(jsonRawData); 13 | expect(dartCode.warnings.length, equals(0)); 14 | expect(dartCode.code.contains('class GlossDiv'), equals(true)); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test_resources/array_root.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "glossary": { 3 | "title": "example glossary", 4 | "GlossDiv": { 5 | "title": "S", 6 | "GlossList": { 7 | "GlossEntry": { 8 | "ID": "SGML", 9 | "SortAs": "SGML", 10 | "GlossTerm": "Standard Generalized Markup Language", 11 | "Acronym": "SGML", 12 | "Abbrev": "ISO 8879:1986", 13 | "GlossDef": { 14 | "para": "A meta-markup language, used to create markup languages such as DocBook.", 15 | "GlossSeeAlso": ["GML", "XML"] 16 | }, 17 | "GlossSee": "markup" 18 | } 19 | } 20 | } 21 | } 22 | }] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | sudo: false 3 | 4 | dart: 5 | - dev 6 | 7 | jobs: 8 | include: 9 | # First, check that everything analyzes properly and is formatted. 10 | - stage: analyze_and_format 11 | script: 12 | - dartanalyzer --fatal-warnings . 13 | - dartfmt -n --set-exit-if-changed . 14 | # Set up several jobs in the next stage, using the built in sharding 15 | # feature from the `test` package. 16 | - stage: unit_test 17 | script: 18 | - pub run test 19 | 20 | # Specify the ordering of your stages 21 | stages: 22 | - analyze_and_format 23 | - unit_test 24 | 25 | cache: 26 | directories: 27 | - $HOME/.pub-cache 28 | - .dart_tool/build 29 | -------------------------------------------------------------------------------- /test/matrix_test.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | // import 'package:json_to_dart/json_to_dart.dart' show ModelGenerator; 5 | 6 | void main() { 7 | group("model-generator", () { 8 | test("Should generate the classes to parse the JSON", () { 9 | // final jsonRawData = 10 | // new File("test_resources/matrix.json").readAsStringSync(); 11 | // final generator = ModelGenerator('Matrix'); 12 | // FIXME: Add matrix support 13 | // final dartCode = generator.generateDartClasses(jsonRawData); 14 | // expect(dartCode.warnings.length, equals(1)); 15 | // expect(dartCode.code.contains('class Matrix'), equals(true)); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/bug_39_test.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | // import 'package:json_to_dart/json_to_dart.dart' show ModelGenerator; 5 | 6 | void main() { 7 | group("model-generator", () { 8 | test("Should generate the classes to parse the JSON", () { 9 | // final jsonRawData = 10 | // new File("test_resources/bug_39.json").readAsStringSync(); 11 | // final generator = ModelGenerator('BugThirtyNine'); 12 | // FIXME: Add matrix support 13 | // final dartCode = generator.generateDartClasses(jsonRawData); 14 | // expect(dartCode.warnings.length, equals(0)); 15 | // expect(dartCode.code.contains('class BugForty'), equals(true)); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "generate_bug_10", 9 | "program": "${workspaceFolder}/example/generate_model_example.dart", 10 | "request": "launch", 11 | "type": "dart" 12 | }, 13 | { 14 | "name": "Unit Tests", 15 | "type": "dart", 16 | "request": "launch", 17 | "program": "${workspaceFolder}/test/model_generator_warnings_test.dart" 18 | }, 19 | ] 20 | } -------------------------------------------------------------------------------- /example/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "javiercbk", 3 | "favouriteInteger": 18, 4 | "favouriteDouble": 1.618, 5 | "url": "https://api.github.com/users/javiercbk", 6 | "html_url": "https://github.com/javiercbk", 7 | "tags": ["dart", "json", "cool"], 8 | "randomIntegers": [1, 2, 3], 9 | "randomDoubles": [1.1, 2.2, 3.3], 10 | "personalInfo": { 11 | "firstName": "Javier", 12 | "lastName": "Lecuona", 13 | "location": "Buenos Aires, Argentina", 14 | "phones": [ 15 | { 16 | "type": "work", 17 | "number": "123-this-is-a-fake-phone", 18 | "shouldCall": false 19 | }, 20 | { 21 | "type": "home", 22 | "number": "123-this-is-a-phony-phone", 23 | "shouldCall": false 24 | } 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/bug_40_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:json_to_dart/json_to_dart.dart' show ModelGenerator; 5 | 6 | void main() { 7 | group("model-generator", () { 8 | test("Should generate the classes to parse the JSON", () { 9 | final jsonRawData = 10 | new File("test_resources/bug_40.json").readAsStringSync(); 11 | final generator = ModelGenerator('BugForty'); 12 | final dartCode = generator.generateDartClasses(jsonRawData); 13 | expect(dartCode.warnings.length, equals(1)); 14 | expect(dartCode.warnings[0].warning, equals("list is empty")); 15 | expect(dartCode.warnings[0].path, equals("/CustomButtons")); 16 | expect(dartCode.code.contains('class BugForty'), equals(true)); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test_resources/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "username": "javiercbk", 3 | "favouriteInteger": 18, 4 | "favouriteDouble": 1.6180, 5 | "url": "https://api.github.com/users/javiercbk", 6 | "html_url": "https://github.com/javiercbk", 7 | "tags": ["dart", "json", "cool"], 8 | "randomIntegers": [1, 2, 3], 9 | "randomDoubles": [1.1, 2.2, 3.3], 10 | "personalInfo": { 11 | "firstName": "Javier", 12 | "lastName": "Lecuona", 13 | "location": "Buenos Aires, Argentina", 14 | "phones": [{ 15 | "type": "work", 16 | "number": "123-this-is-a-fake-phone", 17 | "shouldCall": false 18 | }, 19 | { 20 | "type": "home", 21 | "number": "123-this-is-a-phony-phone", 22 | "shouldCall": false 23 | }] 24 | } 25 | } -------------------------------------------------------------------------------- /example/generate_model_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import "package:path/path.dart" show dirname, join, normalize; 3 | import '../lib/json_to_dart.dart'; 4 | 5 | String _scriptPath() { 6 | var script = Platform.script.toString(); 7 | if (script.startsWith("file://")) { 8 | script = script.substring(7); 9 | } else { 10 | final idx = script.indexOf("file:/"); 11 | script = script.substring(idx + 5); 12 | } 13 | return script; 14 | } 15 | 16 | main() { 17 | final classGenerator = new ModelGenerator('Sample'); 18 | final currentDirectory = dirname(_scriptPath()); 19 | final filePath = normalize(join(currentDirectory, 'sample.json')); 20 | final jsonRawData = new File(filePath).readAsStringSync(); 21 | DartCode dartCode = classGenerator.generateDartClasses(jsonRawData); 22 | print(dartCode.code); 23 | } 24 | -------------------------------------------------------------------------------- /example/generate_model_private_example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import "package:path/path.dart" show dirname, join, normalize; 3 | import '../lib/json_to_dart.dart'; 4 | 5 | String _scriptPath() { 6 | var script = Platform.script.toString(); 7 | if (script.startsWith("file://")) { 8 | script = script.substring(7); 9 | } else { 10 | final idx = script.indexOf("file:/"); 11 | script = script.substring(idx + 5); 12 | } 13 | return script; 14 | } 15 | 16 | main() { 17 | final classGenerator = new ModelGenerator('Sample', true); 18 | final currentDirectory = dirname(_scriptPath()); 19 | final filePath = normalize(join(currentDirectory, 'sample.json')); 20 | final jsonRawData = new File(filePath).readAsStringSync(); 21 | DartCode dartCode = classGenerator.generateDartClasses(jsonRawData); 22 | print(dartCode.code); 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON to Dart 2 | 3 | [![Build Status](https://travis-ci.org/javiercbk/json_to_dart.svg?branch=master)](https://travis-ci.org/javiercbk/json_to_dart) 4 | 5 | Given a JSON string, this library will generate all the necessary Dart classes to parse and generate JSON. 6 | 7 | This library is designed to generate Flutter friendly model classes following the [flutter's doc recommendation](https://flutter.io/json/#serializing-json-manually-using-dartconvert). 8 | 9 | ## Caveats 10 | 11 | - When an empty array is given, it will create a List. Such weird behaviour should warn the user that there is no data to extract. 12 | - Equal structures are not detected yet (Equal classes are going to be created over and over). 13 | - Properties named with funky names (like "!breaks", "|breaks", etc) or keyword (like "this", "break", "class", etc) will produce syntax errors. 14 | - Array of arrays are not supported: 15 | 16 | ```json 17 | [[{ "isThisSupported": false }]] 18 | ``` 19 | 20 | ```json 21 | [{ "thisSupported": [{ "cool": true }] }] 22 | ``` 23 | -------------------------------------------------------------------------------- /test/model_generator_warnings_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:json_to_dart/model_generator.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group("model-generator-with-warnings", () { 7 | test("should generate proper warnings", () { 8 | final jsonRawData = 9 | new File("test_resources/test_warnings.json").readAsStringSync(); 10 | final ModelGenerator modelGenerator = new ModelGenerator("Warnings"); 11 | DartCode dartCode = modelGenerator.generateUnsafeDart(jsonRawData); 12 | expect(dartCode.warnings, isNot(null)); 13 | expect(dartCode.warnings.length, equals(4)); 14 | expect(dartCode.warnings[0].warning, equals("list is ambiguous")); 15 | expect(dartCode.warnings[0].path, equals("/ambiguousArray")); 16 | expect(dartCode.warnings[1].warning, equals("list is empty")); 17 | expect(dartCode.warnings[1].path, equals("/emptyArr")); 18 | expect(dartCode.warnings[2].warning, equals("type is ambiguous")); 19 | expect(dartCode.warnings[2].path, equals("/ambiguous[2]/arr[0]/amb")); 20 | expect(dartCode.warnings[3].warning, equals("list is empty")); 21 | expect(dartCode.warnings[3].path, equals("/ambiguous/arr/emptyArr")); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /test/helpers_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:json_to_dart/helpers.dart'; 3 | import 'package:json_to_dart/syntax.dart'; 4 | 5 | void main() { 6 | group("helpers", () { 7 | test("getInferredType should return the correct types", () { 8 | expect(getInferredType(1), equals(ListType.Int)); 9 | expect(getInferredType(1.1), equals(ListType.Double)); 10 | expect(getInferredType("asd"), equals(ListType.String)); 11 | expect(getInferredType({"a": "a"}), equals(ListType.Object)); 12 | }); 13 | test("camelCase should correctly transform Strings", () { 14 | const Map mappings = const { 15 | 'kebab-case': 'KebabCase', 16 | 'snake_case': 'SnakeCase', 17 | 'CamelCase': 'CamelCase', 18 | 'camelCase': 'CamelCase', 19 | }; 20 | mappings.keys.forEach((key) { 21 | expect(camelCase(key), equals(mappings[key])); 22 | }); 23 | }); 24 | test("camelCaseFirstLower should correctly transform Strings", () { 25 | const Map mappings = const { 26 | 'kebab-case': 'kebabCase', 27 | 'snake_case': 'snakeCase', 28 | 'CamelCase': 'camelCase', 29 | 'camelCase': 'camelCase', 30 | }; 31 | mappings.keys.forEach((key) { 32 | expect(camelCaseFirstLower(key), equals(mappings[key])); 33 | }); 34 | }); 35 | 36 | test("fixFieldName should avoid offending variable names", () { 37 | expect( 38 | fixFieldName('48x48', 39 | typeDef: new TypeDefinition( 40 | 'String', 41 | ), 42 | privateField: false), 43 | equals('s48x48')); 44 | expect( 45 | fixFieldName('_avoidPrivate', 46 | typeDef: new TypeDefinition('String'), privateField: false), 47 | equals('sAvoidPrivate')); 48 | expect( 49 | fixFieldName('48x48', 50 | typeDef: new TypeDefinition('String'), privateField: true), 51 | equals('_s48x48')); 52 | expect( 53 | fixFieldName('_avoidPrivate', 54 | typeDef: new TypeDefinition('String'), privateField: true), 55 | equals('_sAvoidPrivate')); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/double_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:json_ast/json_ast.dart' show LiteralNode; 3 | import 'package:test/test.dart'; 4 | 5 | import 'package:json_to_dart/model_generator.dart'; 6 | import 'package:json_to_dart/helpers.dart' show isASTLiteralDouble; 7 | 8 | void main() { 9 | group("Should identify doubles and ints", () { 10 | test("should parse literals correctly", () { 11 | expect(isASTLiteralDouble(LiteralNode(1, '1')), isFalse); 12 | expect(isASTLiteralDouble(LiteralNode(1e0, '1e0')), isFalse); 13 | expect(isASTLiteralDouble(LiteralNode(1e1, '1e1')), isFalse); 14 | expect(isASTLiteralDouble(LiteralNode(1.1e1, '1.1e1')), isFalse); 15 | expect(isASTLiteralDouble(LiteralNode(10e-1, '10e-1')), isFalse); 16 | expect( 17 | isASTLiteralDouble(LiteralNode(1000.000e-1, '1000.000e-1')), isFalse); 18 | expect(isASTLiteralDouble(LiteralNode(1000.0e-1, '1000.0e-1')), isFalse); 19 | expect(isASTLiteralDouble(LiteralNode(1000.0e-0, '1000.0e-0')), isFalse); 20 | expect( 21 | isASTLiteralDouble(LiteralNode(10.0000e-1, '10.0000e-1')), isFalse); 22 | expect( 23 | isASTLiteralDouble(LiteralNode(10.000000000000000000000000001, 24 | '10.000000000000000000000000001')), 25 | isTrue); 26 | expect(isASTLiteralDouble(LiteralNode(10.0, '10.0')), isTrue); 27 | expect(isASTLiteralDouble(LiteralNode(10.1e0, '10.1e0')), isTrue); 28 | expect(isASTLiteralDouble(LiteralNode(10.01e1, '10.01e1')), isTrue); 29 | expect(isASTLiteralDouble(LiteralNode(11e-1, '11e-1')), isTrue); 30 | expect(isASTLiteralDouble(LiteralNode(11.0000e-1, '11.0000e-1')), isTrue); 31 | expect(isASTLiteralDouble(LiteralNode(0.1, '0.1')), isTrue); 32 | }); 33 | 34 | test("Should identify a double number and generate the proper type", () { 35 | final jsonRawData = 36 | new File("test_resources/double.json").readAsStringSync(); 37 | final modelGenerator = ModelGenerator('DoubleTest'); 38 | final dartSourceCode = modelGenerator.generateDartClasses(jsonRawData); 39 | final wrongDoubleRegExp = RegExp(r"^.*double int[0-9]+;$"); 40 | final wrongIntRegExp = RegExp(r"^.*int double[0-9]+;$"); 41 | final wrongDoubleMatch = 42 | wrongDoubleRegExp.firstMatch(dartSourceCode.code); 43 | final wrongIntMatch = wrongIntRegExp.firstMatch(dartSourceCode.code); 44 | expect(wrongDoubleMatch, isNull, reason: 'Wrong double found'); 45 | expect(wrongIntMatch, isNull, reason: 'Wrong int found'); 46 | }); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/generated/sample.dart: -------------------------------------------------------------------------------- 1 | class Sample { 2 | String? username; 3 | int? favouriteInteger; 4 | double? favouriteDouble; 5 | String? url; 6 | String? htmlUrl; 7 | List? tags; 8 | List? randomIntegers; 9 | List? randomDoubles; 10 | PersonalInfo? personalInfo; 11 | 12 | Sample( 13 | {this.username, 14 | this.favouriteInteger, 15 | this.favouriteDouble, 16 | this.url, 17 | this.htmlUrl, 18 | this.tags, 19 | this.randomIntegers, 20 | this.randomDoubles, 21 | this.personalInfo}); 22 | 23 | Sample.fromJson(Map json) { 24 | username = json['username']; 25 | favouriteInteger = json['favouriteInteger']; 26 | favouriteDouble = json['favouriteDouble']; 27 | url = json['url']; 28 | htmlUrl = json['html_url']; 29 | tags = json['tags'].cast(); 30 | randomIntegers = json['randomIntegers'].cast(); 31 | randomDoubles = json['randomDoubles'].cast(); 32 | personalInfo = json['personalInfo'] != null 33 | ? new PersonalInfo.fromJson(json['personalInfo']) 34 | : null; 35 | } 36 | 37 | Map toJson() { 38 | final Map data = new Map(); 39 | data['username'] = this.username; 40 | data['favouriteInteger'] = this.favouriteInteger; 41 | data['favouriteDouble'] = this.favouriteDouble; 42 | data['url'] = this.url; 43 | data['html_url'] = this.htmlUrl; 44 | data['tags'] = this.tags; 45 | data['randomIntegers'] = this.randomIntegers; 46 | data['randomDoubles'] = this.randomDoubles; 47 | if (this.personalInfo != null) { 48 | data['personalInfo'] = this.personalInfo!.toJson(); 49 | } 50 | return data; 51 | } 52 | } 53 | 54 | class PersonalInfo { 55 | String? firstName; 56 | String? lastName; 57 | String? location; 58 | List? phones; 59 | 60 | PersonalInfo({this.firstName, this.lastName, this.location, this.phones}); 61 | 62 | PersonalInfo.fromJson(Map json) { 63 | firstName = json['firstName']; 64 | lastName = json['lastName']; 65 | location = json['location']; 66 | if (json['phones'] != null) { 67 | phones = []; 68 | json['phones'].forEach((v) { 69 | phones!.add(new Phones.fromJson(v)); 70 | }); 71 | } 72 | } 73 | 74 | Map toJson() { 75 | final Map data = new Map(); 76 | data['firstName'] = this.firstName; 77 | data['lastName'] = this.lastName; 78 | data['location'] = this.location; 79 | if (this.phones != null) { 80 | data['phones'] = this.phones!.map((v) => v.toJson()).toList(); 81 | } 82 | return data; 83 | } 84 | } 85 | 86 | class Phones { 87 | String? type; 88 | String? number; 89 | bool? shouldCall; 90 | 91 | Phones({this.type, this.number, this.shouldCall}); 92 | 93 | Phones.fromJson(Map json) { 94 | type = json['type']; 95 | number = json['number']; 96 | shouldCall = json['shouldCall']; 97 | } 98 | 99 | Map toJson() { 100 | final Map data = new Map(); 101 | data['type'] = this.type; 102 | data['number'] = this.number; 103 | data['shouldCall'] = this.shouldCall; 104 | return data; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /test/generated/bug_ten.dart: -------------------------------------------------------------------------------- 1 | class BugTen { 2 | Glossary? glossary; 3 | 4 | BugTen({this.glossary}); 5 | 6 | BugTen.fromJson(Map json) { 7 | glossary = json['glossary'] != null 8 | ? new Glossary.fromJson(json['glossary']) 9 | : null; 10 | } 11 | 12 | Map toJson() { 13 | final Map data = new Map(); 14 | if (this.glossary != null) { 15 | data['glossary'] = this.glossary!.toJson(); 16 | } 17 | return data; 18 | } 19 | } 20 | 21 | class Glossary { 22 | String? title; 23 | GlossDiv? glossDiv; 24 | 25 | Glossary({this.title, this.glossDiv}); 26 | 27 | Glossary.fromJson(Map json) { 28 | title = json['title']; 29 | glossDiv = json['GlossDiv'] != null 30 | ? new GlossDiv.fromJson(json['GlossDiv']) 31 | : null; 32 | } 33 | 34 | Map toJson() { 35 | final Map data = new Map(); 36 | data['title'] = this.title; 37 | if (this.glossDiv != null) { 38 | data['GlossDiv'] = this.glossDiv!.toJson(); 39 | } 40 | return data; 41 | } 42 | } 43 | 44 | class GlossDiv { 45 | String? title; 46 | GlossList? glossList; 47 | 48 | GlossDiv({this.title, this.glossList}); 49 | 50 | GlossDiv.fromJson(Map json) { 51 | title = json['title']; 52 | glossList = json['GlossList'] != null 53 | ? new GlossList.fromJson(json['GlossList']) 54 | : null; 55 | } 56 | 57 | Map toJson() { 58 | final Map data = new Map(); 59 | data['title'] = this.title; 60 | if (this.glossList != null) { 61 | data['GlossList'] = this.glossList!.toJson(); 62 | } 63 | return data; 64 | } 65 | } 66 | 67 | class GlossList { 68 | GlossEntry? glossEntry; 69 | 70 | GlossList({this.glossEntry}); 71 | 72 | GlossList.fromJson(Map json) { 73 | glossEntry = json['GlossEntry'] != null 74 | ? new GlossEntry.fromJson(json['GlossEntry']) 75 | : null; 76 | } 77 | 78 | Map toJson() { 79 | final Map data = new Map(); 80 | if (this.glossEntry != null) { 81 | data['GlossEntry'] = this.glossEntry!.toJson(); 82 | } 83 | return data; 84 | } 85 | } 86 | 87 | class GlossEntry { 88 | String? iD; 89 | String? sortAs; 90 | String? glossTerm; 91 | String? acronym; 92 | String? abbrev; 93 | GlossDef? glossDef; 94 | String? glossSee; 95 | 96 | GlossEntry( 97 | {this.iD, 98 | this.sortAs, 99 | this.glossTerm, 100 | this.acronym, 101 | this.abbrev, 102 | this.glossDef, 103 | this.glossSee}); 104 | 105 | GlossEntry.fromJson(Map json) { 106 | iD = json['ID']; 107 | sortAs = json['SortAs']; 108 | glossTerm = json['GlossTerm']; 109 | acronym = json['Acronym']; 110 | abbrev = json['Abbrev']; 111 | glossDef = json['GlossDef'] != null 112 | ? new GlossDef.fromJson(json['GlossDef']) 113 | : null; 114 | glossSee = json['GlossSee']; 115 | } 116 | 117 | Map toJson() { 118 | final Map data = new Map(); 119 | data['ID'] = this.iD; 120 | data['SortAs'] = this.sortAs; 121 | data['GlossTerm'] = this.glossTerm; 122 | data['Acronym'] = this.acronym; 123 | data['Abbrev'] = this.abbrev; 124 | if (this.glossDef != null) { 125 | data['GlossDef'] = this.glossDef!.toJson(); 126 | } 127 | data['GlossSee'] = this.glossSee; 128 | return data; 129 | } 130 | } 131 | 132 | class GlossDef { 133 | String? para; 134 | List? glossSeeAlso; 135 | 136 | GlossDef({this.para, this.glossSeeAlso}); 137 | 138 | GlossDef.fromJson(Map json) { 139 | para = json['para']; 140 | glossSeeAlso = json['GlossSeeAlso'].cast(); 141 | } 142 | 143 | Map toJson() { 144 | final Map data = new Map(); 145 | data['para'] = this.para; 146 | data['GlossSeeAlso'] = this.glossSeeAlso; 147 | return data; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /test/bug_10_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | 4 | import 'package:test/test.dart'; 5 | import 'package:json_to_dart/json_to_dart.dart' show ModelGenerator; 6 | import './generated/bug_ten.dart'; 7 | 8 | void main() { 9 | group("model-generator", () { 10 | test("Should generate the classes to parse the JSON", () { 11 | final jsonRawData = 12 | new File("test_resources/bug_10.json").readAsStringSync(); 13 | final generator = ModelGenerator('BugTen'); 14 | final dartCode = generator.generateDartClasses(jsonRawData); 15 | expect(dartCode.warnings.length, equals(0)); 16 | expect(dartCode.code.contains('class GlossDiv'), equals(true)); 17 | }); 18 | 19 | test("Generated class should correctly parse JSON for bug 10", () { 20 | final jsonRawData = 21 | new File("test_resources/bug_10.json").readAsStringSync(); 22 | Map sampleMap = json.decode(jsonRawData); 23 | final bugTen = new BugTen.fromJson(sampleMap); 24 | expect(bugTen, isNot(isNull)); 25 | expect(bugTen.glossary, isNot(isNull)); 26 | expect(bugTen.glossary!.title, equals('example glossary')); 27 | expect(bugTen.glossary!.glossDiv, isNot(isNull)); 28 | expect(bugTen.glossary!.glossDiv!.title, equals("S")); 29 | expect(bugTen.glossary!.glossDiv!.glossList, isNot(isNull)); 30 | final ge = bugTen.glossary!.glossDiv!.glossList!.glossEntry; 31 | expect(ge, isNot(isNull)); 32 | expect(ge!.iD, equals("SGML")); 33 | expect(ge.sortAs, equals("SGML")); 34 | expect(ge.glossTerm, equals("Standard Generalized Markup Language")); 35 | expect(ge.acronym, equals("SGML")); 36 | expect(ge.abbrev, equals("ISO 8879:1986")); 37 | expect(ge.glossSee, equals("markup")); 38 | expect(ge.glossDef, isNot(isNull)); 39 | expect( 40 | ge.glossDef!.para, 41 | equals( 42 | "A meta-markup language, used to create markup languages such as DocBook.")); 43 | final seeAlso = ge.glossDef!.glossSeeAlso; 44 | expect(seeAlso, isNot(isNull)); 45 | expect(seeAlso!.length, equals(2)); 46 | expect(seeAlso[0], equals("GML")); 47 | expect(seeAlso[1], equals("XML")); 48 | }); 49 | 50 | test("Generated class should correctly generate JSON", () { 51 | final glossSeeAlso = []; 52 | glossSeeAlso.add("GML"); 53 | glossSeeAlso.add("XML"); 54 | final glossDef = new GlossDef( 55 | para: 56 | "A meta-markup language, used to create markup languages such as DocBook.", 57 | glossSeeAlso: glossSeeAlso); 58 | final glossEntry = new GlossEntry( 59 | abbrev: "ISO 8879:1986", 60 | acronym: "SGML", 61 | glossDef: glossDef, 62 | glossSee: "markup", 63 | glossTerm: "Standard Generalized Markup Language", 64 | iD: "SGML", 65 | sortAs: "SGML", 66 | ); 67 | final glossList = new GlossList( 68 | glossEntry: glossEntry, 69 | ); 70 | final glossDiv = new GlossDiv( 71 | glossList: glossList, 72 | title: "S", 73 | ); 74 | final glossary = new Glossary( 75 | glossDiv: glossDiv, 76 | title: "example glossary", 77 | ); 78 | final bugTen = new BugTen(glossary: glossary); 79 | final codec = new JsonCodec(toEncodable: (dynamic v) => v.toString()); 80 | final encodedJSON = codec.encode(bugTen.toJson()); 81 | expect(encodedJSON.contains('"title":"example glossary"'), equals(true)); 82 | expect(encodedJSON.contains('"GlossDiv":{"title":"S"'), equals(true)); 83 | expect(encodedJSON.contains('"GlossList":{"GlossEntry":{'), equals(true)); 84 | expect(encodedJSON.contains('"ID":"SGML",'), equals(true)); 85 | expect(encodedJSON.contains('"SortAs":"SGML",'), equals(true)); 86 | expect( 87 | encodedJSON 88 | .contains('"GlossTerm":"Standard Generalized Markup Language",'), 89 | equals(true)); 90 | expect(encodedJSON.contains('"Acronym":"SGML",'), equals(true)); 91 | expect(encodedJSON.contains('"Abbrev":"ISO 8879:1986",'), equals(true)); 92 | expect( 93 | encodedJSON.contains( 94 | '"GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook.",'), 95 | equals(true)); 96 | expect( 97 | encodedJSON.contains('"GlossSeeAlso":["GML","XML"]'), equals(true)); 98 | expect(encodedJSON.contains('"GlossSee":"markup"'), equals(true)); 99 | }); 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /test_resources/bug_40.json: -------------------------------------------------------------------------------- 1 | { 2 | "SessionExpire": 240, 3 | "StayLoggedInTime": 10000, 4 | "StayLoggedInDeviceType": 3, 5 | "CanShareAsNews": true, 6 | "NewsFeedSummaryLength": 300, 7 | "ApplicationLanguageAccess": [1], 8 | "GAAccount": "UA-75836431-2", 9 | "FileUploadLimit": 50, 10 | "FileTypeExtensions": { 11 | ".XPM": 12, 12 | ".JP2": 12, 13 | ".HTML": 8 14 | }, 15 | "CurrentVersion": { 16 | "Id": 133, 17 | "VersionMajor": 129, 18 | "VersionMinor": 5, 19 | "UpgradeOn": "2019-01-24T16:13:00+01:00", 20 | "UpgradeEndTime": "2019-01-24T16:23:00+01:00", 21 | "UpgradeVersionType": 1, 22 | "InternalDescription": null, 23 | "CustomerInformation": "Denna uppgradering innehåller buggfixar.", 24 | "VersionNumberText": "0.0.129.5", 25 | "UpgradeStatusType": 2, 26 | "SecondsUntilUpgradeStart": -24180091, 27 | "CreatedByUserId": 7904, 28 | "CreatedOn": "2019-01-24T15:14:18+01:00", 29 | "ChangedByUserId": 7904, 30 | "ChangedOn": "2019-01-24T15:14:18+01:00", 31 | "CreatedByUserName": "Mattias Andersson", 32 | "ChangedByUserName": "Mattias Andersson" 33 | }, 34 | "NumberOfDaysTillNextUpgrade": 2147483647, 35 | "UserIPAddress": "78.159.99.208", 36 | "Senders": ["Chain SMS Sender"], 37 | "CurrentCulture": "en-US", 38 | "UserApplicationLanguageType": 2, 39 | "ShowStayLoggedIn": true, 40 | "IsRequestAccessAvailable": true, 41 | "SupportPhoneNumber": "+380666901413", 42 | "SupportEmailAddress": "mattias@chainformation.com", 43 | "CustomerId": 25, 44 | "CustomerName": "Chainformation", 45 | "MobileAppName": "Development App", 46 | "DefaultApplicationLanguageType": 2, 47 | "AdConfigs": null, 48 | "CustomButtons": [], 49 | "CustomerSettings": { 50 | "Id": 1, 51 | "BrandingColor": "27ae60", 52 | "LogoReference": { 53 | "Id": 2377, 54 | "ObjectId": 1, 55 | "MediaBankFileId": 4552, 56 | "ObjectType": 58, 57 | "IsDirectUpload": false, 58 | "PhysicalFileName": "98fe2958-5a34-4e75-be45-0f470b65ef41", 59 | "Extension": ".jpg", 60 | "Name": "images (1)", 61 | "FileType": 1, 62 | "Width": 300, 63 | "Height": 168, 64 | "CreatedOn": "2019-01-14T15:20:10+01:00", 65 | "CreatedByUserId": 7904, 66 | "CreatedByUserName": "Mattias Andersson", 67 | "CreatedByMediaBankPhysicalFileName": "d6f1b7d4-3437-4544-8eb1-4876b3af0502", 68 | "ContentType": "image/jpeg", 69 | "ReferenceType": 2 70 | }, 71 | "LandscapeReference": { 72 | "Id": 2378, 73 | "ObjectId": 1, 74 | "MediaBankFileId": 4553, 75 | "ObjectType": 58, 76 | "IsDirectUpload": false, 77 | "PhysicalFileName": "077114c6-1ef1-49ae-90e2-c01c1b5954fb", 78 | "Extension": ".jpg", 79 | "Name": "Bos_grunnien", 80 | "FileType": 1, 81 | "Width": 3264, 82 | "Height": 2448, 83 | "CreatedOn": "2019-01-14T15:20:18+01:00", 84 | "CreatedByUserId": 7904, 85 | "CreatedByUserName": "Mattias Andersson", 86 | "CreatedByMediaBankPhysicalFileName": "d6f1b7d4-3437-4544-8eb1-4876b3af0502", 87 | "ContentType": "image/jpeg", 88 | "ReferenceType": 3 89 | }, 90 | "PortraitReference": { 91 | "Id": 2379, 92 | "ObjectId": 1, 93 | "MediaBankFileId": 4554, 94 | "ObjectType": 58, 95 | "IsDirectUpload": false, 96 | "PhysicalFileName": "9bec8dd7-602c-4aa8-b0f6-00277e077604", 97 | "Extension": ".jpg", 98 | "Name": "165", 99 | "FileType": 1, 100 | "Width": 900, 101 | "Height": 600, 102 | "CreatedOn": "2019-01-14T15:20:30+01:00", 103 | "CreatedByUserId": 7904, 104 | "CreatedByUserName": "Mattias Andersson", 105 | "CreatedByMediaBankPhysicalFileName": "d6f1b7d4-3437-4544-8eb1-4876b3af0502", 106 | "ContentType": "image/jpeg", 107 | "ReferenceType": 4 108 | } 109 | }, 110 | "UpcomingVersion": null, 111 | "ChecklistPresenceEnabled": false, 112 | "StorageDomain": "https://ccm2westeurope.blob.core.windows.net", 113 | "ApplicationSettingAccessTypes": [ 114 | { 115 | "ApplicationSettingType": 103, 116 | "AccessRightTypes": [40] 117 | }, 118 | { 119 | "ApplicationSettingType": 104, 120 | "AccessRightTypes": [40] 121 | }, 122 | { 123 | "ApplicationSettingType": 105, 124 | "AccessRightTypes": [10, 20, 30, 40] 125 | } 126 | ], 127 | "ContentLanguages": [ 128 | { 129 | "Id": 1, 130 | "IsDefaultLanguage": false 131 | }, 132 | { 133 | "Id": 2, 134 | "IsDefaultLanguage": true 135 | }, 136 | { 137 | "Id": 4, 138 | "IsDefaultLanguage": false 139 | }, 140 | { 141 | "Id": 10, 142 | "IsDefaultLanguage": false 143 | } 144 | ] 145 | } 146 | -------------------------------------------------------------------------------- /lib/model_generator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:dart_style/dart_style.dart'; 4 | import 'package:json_ast/json_ast.dart' show parse, Settings, Node; 5 | import 'package:json_to_dart/helpers.dart'; 6 | import 'package:json_to_dart/syntax.dart'; 7 | 8 | class DartCode extends WithWarning { 9 | DartCode(String result, List warnings) : super(result, warnings); 10 | 11 | String get code => this.result; 12 | } 13 | 14 | /// A Hint is a user type correction. 15 | class Hint { 16 | final String path; 17 | final String type; 18 | 19 | Hint(this.path, this.type); 20 | } 21 | 22 | class ModelGenerator { 23 | final String _rootClassName; 24 | final bool _privateFields; 25 | List allClasses = []; 26 | final Map sameClassMapping = new HashMap(); 27 | late List hints; 28 | 29 | ModelGenerator(this._rootClassName, [this._privateFields = false, hints]) { 30 | if (hints != null) { 31 | this.hints = hints; 32 | } else { 33 | this.hints = []; 34 | } 35 | } 36 | 37 | Hint? _hintForPath(String path) { 38 | final hint = this 39 | .hints 40 | .firstWhere((h) => h.path == path, orElse: () => Hint("", "")); 41 | if (hint.path == "") { 42 | return null; 43 | } 44 | } 45 | 46 | List _generateClassDefinition(String className, 47 | dynamic jsonRawDynamicData, String path, Node? astNode) { 48 | List warnings = []; 49 | if (jsonRawDynamicData is List) { 50 | // if first element is an array, start in the first element. 51 | final node = navigateNode(astNode, '0'); 52 | _generateClassDefinition(className, jsonRawDynamicData[0], path, node!); 53 | } else { 54 | final Map jsonRawData = jsonRawDynamicData; 55 | final keys = jsonRawData.keys; 56 | ClassDefinition classDefinition = 57 | new ClassDefinition(className, _privateFields); 58 | keys.forEach((key) { 59 | TypeDefinition typeDef; 60 | final hint = _hintForPath('$path/$key'); 61 | final node = navigateNode(astNode, key); 62 | if (hint != null) { 63 | typeDef = new TypeDefinition(hint.type, astNode: node); 64 | } else { 65 | typeDef = new TypeDefinition.fromDynamic(jsonRawData[key], node); 66 | } 67 | if (typeDef.name == 'Class') { 68 | typeDef.name = camelCase(key); 69 | } 70 | if (typeDef.name == 'List' && typeDef.subtype == 'Null') { 71 | warnings.add(newEmptyListWarn('$path/$key')); 72 | } 73 | if (typeDef.subtype != null && typeDef.subtype == 'Class') { 74 | typeDef.subtype = camelCase(key); 75 | } 76 | if (typeDef.isAmbiguous) { 77 | warnings.add(newAmbiguousListWarn('$path/$key')); 78 | } 79 | classDefinition.addField(key, typeDef); 80 | }); 81 | final similarClass = allClasses.firstWhere((cd) => cd == classDefinition, 82 | orElse: () => ClassDefinition("")); 83 | if (similarClass.name != "") { 84 | final similarClassName = similarClass.name; 85 | final currentClassName = classDefinition.name; 86 | sameClassMapping[currentClassName] = similarClassName; 87 | } else { 88 | allClasses.add(classDefinition); 89 | } 90 | final dependencies = classDefinition.dependencies; 91 | dependencies.forEach((dependency) { 92 | List warns = []; 93 | if (dependency.typeDef.name == 'List') { 94 | // only generate dependency class if the array is not empty 95 | if (jsonRawData[dependency.name].length > 0) { 96 | // when list has ambiguous values, take the first one, otherwise merge all objects 97 | // into a single one 98 | dynamic toAnalyze; 99 | if (!dependency.typeDef.isAmbiguous) { 100 | WithWarning mergeWithWarning = mergeObjectList( 101 | jsonRawData[dependency.name], '$path/${dependency.name}'); 102 | toAnalyze = mergeWithWarning.result; 103 | warnings.addAll(mergeWithWarning.warnings); 104 | } else { 105 | toAnalyze = jsonRawData[dependency.name][0]; 106 | } 107 | final node = navigateNode(astNode, dependency.name); 108 | warns = _generateClassDefinition(dependency.className, toAnalyze, 109 | '$path/${dependency.name}', node); 110 | } 111 | } else { 112 | final node = navigateNode(astNode, dependency.name); 113 | warns = _generateClassDefinition(dependency.className, 114 | jsonRawData[dependency.name], '$path/${dependency.name}', node); 115 | } 116 | warnings.addAll(warns); 117 | }); 118 | } 119 | return warnings; 120 | } 121 | 122 | /// generateUnsafeDart will generate all classes and append one after another 123 | /// in a single string. The [rawJson] param is assumed to be a properly 124 | /// formatted JSON string. The dart code is not validated so invalid dart code 125 | /// might be returned 126 | DartCode generateUnsafeDart(String rawJson) { 127 | final jsonRawData = decodeJSON(rawJson); 128 | final astNode = parse(rawJson, Settings()); 129 | List warnings = 130 | _generateClassDefinition(_rootClassName, jsonRawData, "", astNode); 131 | // after generating all classes, replace the omited similar classes. 132 | allClasses.forEach((c) { 133 | final fieldsKeys = c.fields.keys; 134 | fieldsKeys.forEach((f) { 135 | final typeForField = c.fields[f]; 136 | if (typeForField != null) { 137 | if (sameClassMapping.containsKey(typeForField.name)) { 138 | c.fields[f]!.name = sameClassMapping[typeForField.name]!; 139 | } 140 | } 141 | }); 142 | }); 143 | return new DartCode( 144 | allClasses.map((c) => c.toString()).join('\n'), warnings); 145 | } 146 | 147 | /// generateDartClasses will generate all classes and append one after another 148 | /// in a single string. The [rawJson] param is assumed to be a properly 149 | /// formatted JSON string. If the generated dart is invalid it will throw an error. 150 | DartCode generateDartClasses(String rawJson) { 151 | final unsafeDartCode = generateUnsafeDart(rawJson); 152 | final formatter = new DartFormatter(); 153 | return new DartCode( 154 | formatter.format(unsafeDartCode.code), unsafeDartCode.warnings); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/generated/sample_private.dart: -------------------------------------------------------------------------------- 1 | class Sample { 2 | String? _username; 3 | int? _favouriteInteger; 4 | double? _favouriteDouble; 5 | String? _url; 6 | String? _htmlUrl; 7 | List? _tags; 8 | List? _randomIntegers; 9 | List? _randomDoubles; 10 | PersonalInfo? _personalInfo; 11 | 12 | Sample( 13 | {String? username, 14 | int? favouriteInteger, 15 | double? favouriteDouble, 16 | String? url, 17 | String? htmlUrl, 18 | List? tags, 19 | List? randomIntegers, 20 | List? randomDoubles, 21 | PersonalInfo? personalInfo}) { 22 | if (username != null) { 23 | this._username = username; 24 | } 25 | if (favouriteInteger != null) { 26 | this._favouriteInteger = favouriteInteger; 27 | } 28 | if (favouriteDouble != null) { 29 | this._favouriteDouble = favouriteDouble; 30 | } 31 | if (url != null) { 32 | this._url = url; 33 | } 34 | if (htmlUrl != null) { 35 | this._htmlUrl = htmlUrl; 36 | } 37 | if (tags != null) { 38 | this._tags = tags; 39 | } 40 | if (randomIntegers != null) { 41 | this._randomIntegers = randomIntegers; 42 | } 43 | if (randomDoubles != null) { 44 | this._randomDoubles = randomDoubles; 45 | } 46 | if (personalInfo != null) { 47 | this._personalInfo = personalInfo; 48 | } 49 | } 50 | 51 | String? get username => _username; 52 | set username(String? username) => _username = username; 53 | int? get favouriteInteger => _favouriteInteger; 54 | set favouriteInteger(int? favouriteInteger) => 55 | _favouriteInteger = favouriteInteger; 56 | double? get favouriteDouble => _favouriteDouble; 57 | set favouriteDouble(double? favouriteDouble) => 58 | _favouriteDouble = favouriteDouble; 59 | String? get url => _url; 60 | set url(String? url) => _url = url; 61 | String? get htmlUrl => _htmlUrl; 62 | set htmlUrl(String? htmlUrl) => _htmlUrl = htmlUrl; 63 | List? get tags => _tags; 64 | set tags(List? tags) => _tags = tags; 65 | List? get randomIntegers => _randomIntegers; 66 | set randomIntegers(List? randomIntegers) => 67 | _randomIntegers = randomIntegers; 68 | List? get randomDoubles => _randomDoubles; 69 | set randomDoubles(List? randomDoubles) => 70 | _randomDoubles = randomDoubles; 71 | PersonalInfo? get personalInfo => _personalInfo; 72 | set personalInfo(PersonalInfo? personalInfo) => _personalInfo = personalInfo; 73 | 74 | Sample.fromJson(Map json) { 75 | _username = json['username']; 76 | _favouriteInteger = json['favouriteInteger']; 77 | _favouriteDouble = json['favouriteDouble']; 78 | _url = json['url']; 79 | _htmlUrl = json['html_url']; 80 | _tags = json['tags'].cast(); 81 | _randomIntegers = json['randomIntegers'].cast(); 82 | _randomDoubles = json['randomDoubles'].cast(); 83 | _personalInfo = json['personalInfo'] != null 84 | ? new PersonalInfo.fromJson(json['personalInfo']) 85 | : null; 86 | } 87 | 88 | Map toJson() { 89 | final Map data = new Map(); 90 | data['username'] = this._username; 91 | data['favouriteInteger'] = this._favouriteInteger; 92 | data['favouriteDouble'] = this._favouriteDouble; 93 | data['url'] = this._url; 94 | data['html_url'] = this._htmlUrl; 95 | data['tags'] = this._tags; 96 | data['randomIntegers'] = this._randomIntegers; 97 | data['randomDoubles'] = this._randomDoubles; 98 | if (this._personalInfo != null) { 99 | data['personalInfo'] = this._personalInfo!.toJson(); 100 | } 101 | return data; 102 | } 103 | } 104 | 105 | class PersonalInfo { 106 | String? _firstName; 107 | String? _lastName; 108 | String? _location; 109 | List? _phones; 110 | 111 | PersonalInfo( 112 | {String? firstName, 113 | String? lastName, 114 | String? location, 115 | List? phones}) { 116 | if (firstName != null) { 117 | this._firstName = firstName; 118 | } 119 | if (lastName != null) { 120 | this._lastName = lastName; 121 | } 122 | if (location != null) { 123 | this._location = location; 124 | } 125 | if (phones != null) { 126 | this._phones = phones; 127 | } 128 | } 129 | 130 | String? get firstName => _firstName; 131 | set firstName(String? firstName) => _firstName = firstName; 132 | String? get lastName => _lastName; 133 | set lastName(String? lastName) => _lastName = lastName; 134 | String? get location => _location; 135 | set location(String? location) => _location = location; 136 | List? get phones => _phones; 137 | set phones(List? phones) => _phones = phones; 138 | 139 | PersonalInfo.fromJson(Map json) { 140 | _firstName = json['firstName']; 141 | _lastName = json['lastName']; 142 | _location = json['location']; 143 | if (json['phones'] != null) { 144 | _phones = []; 145 | json['phones'].forEach((v) { 146 | _phones!.add(new Phones.fromJson(v)); 147 | }); 148 | } 149 | } 150 | 151 | Map toJson() { 152 | final Map data = new Map(); 153 | data['firstName'] = this._firstName; 154 | data['lastName'] = this._lastName; 155 | data['location'] = this._location; 156 | if (this._phones != null) { 157 | data['phones'] = this._phones!.map((v) => v.toJson()).toList(); 158 | } 159 | return data; 160 | } 161 | } 162 | 163 | class Phones { 164 | String? _type; 165 | String? _number; 166 | bool? _shouldCall; 167 | 168 | Phones({String? type, String? number, bool? shouldCall}) { 169 | if (type != null) { 170 | this._type = type; 171 | } 172 | if (number != null) { 173 | this._number = number; 174 | } 175 | if (shouldCall != null) { 176 | this._shouldCall = shouldCall; 177 | } 178 | } 179 | 180 | String? get type => _type; 181 | set type(String? type) => _type = type; 182 | String? get number => _number; 183 | set number(String? number) => _number = number; 184 | bool? get shouldCall => _shouldCall; 185 | set shouldCall(bool? shouldCall) => _shouldCall = shouldCall; 186 | 187 | Phones.fromJson(Map json) { 188 | _type = json['type']; 189 | _number = json['number']; 190 | _shouldCall = json['shouldCall']; 191 | } 192 | 193 | Map toJson() { 194 | final Map data = new Map(); 195 | data['type'] = this._type; 196 | data['number'] = this._number; 197 | data['shouldCall'] = this._shouldCall; 198 | return data; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /test/model_generator_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'package:test/test.dart'; 4 | import './generated/sample.dart'; 5 | 6 | void main() { 7 | group("model-generator", () { 8 | test("Generated class should correctly parse JSON", () { 9 | final jsonRawData = 10 | new File("test_resources/test.json").readAsStringSync(); 11 | Map sampleMap = json.decode(jsonRawData); 12 | final sample = new Sample.fromJson(sampleMap); 13 | expect(sample, isNot(isNull)); 14 | expect(sample.username, equals('javiercbk')); 15 | expect(sample.favouriteInteger, equals(18)); 16 | expect(sample.favouriteDouble, equals(1.6180)); 17 | expect(sample.url, equals('https://api.github.com/users/javiercbk')); 18 | expect(sample.htmlUrl, equals('https://github.com/javiercbk')); 19 | expect(sample.tags, isNot(isNull)); 20 | expect(sample.tags!.length, equals(3)); 21 | expect(sample.tags![0], equals('dart')); 22 | expect(sample.tags![1], equals('json')); 23 | expect(sample.tags![2], equals('cool')); 24 | expect(sample.randomIntegers, isNot(isNull)); 25 | expect(sample.randomIntegers!.length, equals(3)); 26 | expect(sample.randomIntegers![0], equals(1)); 27 | expect(sample.randomIntegers![1], equals(2)); 28 | expect(sample.randomIntegers![2], equals(3)); 29 | expect(sample.randomDoubles, isNot(isNull)); 30 | expect(sample.randomDoubles!.length, equals(3)); 31 | expect(sample.randomDoubles![0], equals(1.1)); 32 | expect(sample.randomDoubles![1], equals(2.2)); 33 | expect(sample.randomDoubles![2], equals(3.3)); 34 | final pi = sample.personalInfo; 35 | expect(pi, isNot(isNull)); 36 | expect(pi!.firstName, equals('Javier')); 37 | expect(pi.lastName, equals('Lecuona')); 38 | expect(pi.location, equals('Buenos Aires, Argentina')); 39 | final ph = pi.phones; 40 | expect(ph, isNot(isNull)); 41 | expect(ph!.length, equals(2)); 42 | expect(ph[0], isNot(isNull)); 43 | expect(ph[0].type, equals('work')); 44 | expect(ph[0].number, equals('123-this-is-a-fake-phone')); 45 | expect(ph[0].shouldCall, equals(false)); 46 | expect(ph[1], isNot(isNull)); 47 | expect(ph[1].type, equals('home')); 48 | expect(ph[1].number, equals('123-this-is-a-phony-phone')); 49 | expect(ph[1].shouldCall, equals(false)); 50 | }); 51 | 52 | test("Generated class should correctly parse JSON with missing values", () { 53 | final jsonRawData = 54 | new File("test_resources/test_missing.json").readAsStringSync(); 55 | Map sampleMap = json.decode(jsonRawData); 56 | final sample = new Sample.fromJson(sampleMap); 57 | expect(sample, isNot(isNull)); 58 | expect(sample.username, equals('javiercbk')); 59 | expect(sample.favouriteInteger, isNull); 60 | expect(sample.favouriteDouble, equals(1.6180)); 61 | expect(sample.url, equals('https://api.github.com/users/javiercbk')); 62 | expect(sample.htmlUrl, isNull); 63 | expect(sample.tags, isNot(isNull)); 64 | expect(sample.tags!.length, equals(3)); 65 | expect(sample.tags![0], equals('dart')); 66 | expect(sample.tags![1], equals('json')); 67 | expect(sample.tags![2], equals('cool')); 68 | expect(sample.randomIntegers, isNot(isNull)); 69 | expect(sample.randomIntegers!.length, equals(3)); 70 | expect(sample.randomIntegers![0], equals(1)); 71 | expect(sample.randomIntegers![1], equals(2)); 72 | expect(sample.randomIntegers![2], equals(3)); 73 | expect(sample.randomDoubles, isNot(isNull)); 74 | expect(sample.randomDoubles!.length, equals(3)); 75 | expect(sample.randomDoubles![0], equals(1.1)); 76 | expect(sample.randomDoubles![1], equals(2.2)); 77 | expect(sample.randomDoubles![2], equals(3.3)); 78 | final pi = sample.personalInfo; 79 | expect(pi, isNot(isNull)); 80 | expect(pi!.firstName, equals('Javier')); 81 | expect(pi.lastName, isNull); 82 | expect(pi.location, isNull); 83 | expect(pi.phones, isNull); 84 | }); 85 | 86 | test("Generated class should correctly generate JSON", () { 87 | final phones = []; 88 | final phone = new Phones( 89 | type: "IP", 90 | number: "127.0.0.1", 91 | shouldCall: true, 92 | ); 93 | phones.add(phone); 94 | final personalInfo = new PersonalInfo( 95 | firstName: "User", 96 | lastName: "Test", 97 | location: "In a computer", 98 | phones: phones, 99 | ); 100 | final sample = new Sample( 101 | username: 'Test', 102 | favouriteInteger: 13, 103 | favouriteDouble: 3.1416, 104 | url: 'http://test.test', 105 | htmlUrl: 'http://anothertest.test', 106 | tags: const ['test1'], 107 | randomIntegers: const [4, 5], 108 | randomDoubles: const [4.4, 5.5], 109 | personalInfo: personalInfo, 110 | ); 111 | final codec = new JsonCodec(toEncodable: (dynamic v) => v.toString()); 112 | final encodedJSON = codec.encode(sample.toJson()); 113 | expect(encodedJSON.contains('"username":"Test"'), equals(true)); 114 | expect(encodedJSON.contains('"favouriteInteger":13'), equals(true)); 115 | expect(encodedJSON.contains('"favouriteDouble":3.1416'), equals(true)); 116 | expect(encodedJSON.contains('"url":"http://test.test"'), equals(true)); 117 | expect(encodedJSON.contains('"html_url":"http://anothertest.test"'), 118 | equals(true)); 119 | expect(encodedJSON.contains('"tags":["test1"]'), equals(true)); 120 | expect(encodedJSON.contains('"randomIntegers":[4,5]'), equals(true)); 121 | expect(encodedJSON.contains('"randomDoubles":[4.4,5.5]'), equals(true)); 122 | expect(encodedJSON.contains('"personalInfo":{'), equals(true)); 123 | expect(encodedJSON.contains('"firstName":"User"'), equals(true)); 124 | expect(encodedJSON.contains('"lastName":"Test"'), equals(true)); 125 | expect(encodedJSON.contains('"location":"In a computer"'), equals(true)); 126 | expect(encodedJSON.contains('"phones":['), equals(true)); 127 | expect(encodedJSON.contains('"type":"IP"'), equals(true)); 128 | expect(encodedJSON.contains('"number":"127.0.0.1"'), equals(true)); 129 | expect(encodedJSON.contains('"shouldCall":true'), equals(true)); 130 | }); 131 | 132 | test("Generated class should correctly generate JSON with missing values", 133 | () { 134 | final personalInfo = new PersonalInfo(firstName: "User"); 135 | final sample = new Sample( 136 | username: 'Test', 137 | favouriteInteger: null, 138 | favouriteDouble: 3.1416, 139 | url: 'http://test.test', 140 | tags: const ['test1'], 141 | randomIntegers: const [4, 5], 142 | randomDoubles: const [4.4, 5.5], 143 | personalInfo: personalInfo, 144 | ); 145 | final codec = new JsonCodec(toEncodable: (dynamic v) => v.toString()); 146 | final encodedJSON = codec.encode(sample.toJson()); 147 | expect(encodedJSON.contains('"username":"Test"'), equals(true)); 148 | expect(encodedJSON.contains('"favouriteInteger":null'), equals(true)); 149 | expect(encodedJSON.contains('"favouriteDouble":3.1416'), equals(true)); 150 | expect(encodedJSON.contains('"url":"http://test.test"'), equals(true)); 151 | expect(encodedJSON.contains('"html_url":null'), equals(true)); 152 | expect(encodedJSON.contains('"tags":["test1"]'), equals(true)); 153 | expect(encodedJSON.contains('"randomIntegers":[4,5]'), equals(true)); 154 | expect(encodedJSON.contains('"randomDoubles":[4.4,5.5]'), equals(true)); 155 | expect(encodedJSON.contains('"personalInfo":{'), equals(true)); 156 | expect(encodedJSON.contains('"firstName":"User"'), equals(true)); 157 | expect(encodedJSON.contains('"lastName":null'), equals(true)); 158 | expect(encodedJSON.contains('"location":null'), equals(true)); 159 | expect(encodedJSON.contains('"phones"'), equals(false)); 160 | }); 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /test/model_generator_private_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'package:test/test.dart'; 4 | import './generated/sample_private.dart'; 5 | 6 | void main() { 7 | group("model-generator", () { 8 | test("Generated class with private fields should correctly parse JSON", () { 9 | final jsonRawData = 10 | new File("test_resources/test.json").readAsStringSync(); 11 | Map sampleMap = json.decode(jsonRawData); 12 | final sample = new Sample.fromJson(sampleMap); 13 | expect(sample, isNot(isNull)); 14 | expect(sample.username, equals('javiercbk')); 15 | expect(sample.favouriteInteger, equals(18)); 16 | expect(sample.favouriteDouble, equals(1.6180)); 17 | expect(sample.url, equals('https://api.github.com/users/javiercbk')); 18 | expect(sample.htmlUrl, equals('https://github.com/javiercbk')); 19 | expect(sample.tags, isNot(isNull)); 20 | expect(sample.tags!.length, equals(3)); 21 | expect(sample.tags![0], equals('dart')); 22 | expect(sample.tags![1], equals('json')); 23 | expect(sample.tags![2], equals('cool')); 24 | expect(sample.randomIntegers, isNot(isNull)); 25 | expect(sample.randomIntegers!.length, equals(3)); 26 | expect(sample.randomIntegers![0], equals(1)); 27 | expect(sample.randomIntegers![1], equals(2)); 28 | expect(sample.randomIntegers![2], equals(3)); 29 | expect(sample.randomDoubles, isNot(isNull)); 30 | expect(sample.randomDoubles!.length, equals(3)); 31 | expect(sample.randomDoubles![0], equals(1.1)); 32 | expect(sample.randomDoubles![1], equals(2.2)); 33 | expect(sample.randomDoubles![2], equals(3.3)); 34 | final pi = sample.personalInfo; 35 | expect(pi, isNot(isNull)); 36 | expect(pi!.firstName, equals('Javier')); 37 | expect(pi.lastName, equals('Lecuona')); 38 | expect(pi.location, equals('Buenos Aires, Argentina')); 39 | final ph = pi.phones; 40 | expect(ph, isNot(isNull)); 41 | expect(ph!.length, equals(2)); 42 | expect(ph[0], isNot(isNull)); 43 | expect(ph[0].type, equals('work')); 44 | expect(ph[0].number, equals('123-this-is-a-fake-phone')); 45 | expect(ph[0].shouldCall, equals(false)); 46 | expect(ph[1], isNot(isNull)); 47 | expect(ph[1].type, equals('home')); 48 | expect(ph[1].number, equals('123-this-is-a-phony-phone')); 49 | expect(ph[1].shouldCall, equals(false)); 50 | }); 51 | 52 | test( 53 | "Generated class with private fields should correctly parse JSON with missing values", 54 | () { 55 | final jsonRawData = 56 | new File("test_resources/test_missing.json").readAsStringSync(); 57 | Map sampleMap = json.decode(jsonRawData); 58 | final sample = new Sample.fromJson(sampleMap); 59 | expect(sample, isNot(isNull)); 60 | expect(sample.username, equals('javiercbk')); 61 | expect(sample.favouriteInteger, isNull); 62 | expect(sample.favouriteDouble, equals(1.6180)); 63 | expect(sample.url, equals('https://api.github.com/users/javiercbk')); 64 | expect(sample.htmlUrl, isNull); 65 | expect(sample.tags, isNot(isNull)); 66 | expect(sample.tags!.length, equals(3)); 67 | expect(sample.tags![0], equals('dart')); 68 | expect(sample.tags![1], equals('json')); 69 | expect(sample.tags![2], equals('cool')); 70 | expect(sample.randomIntegers, isNot(isNull)); 71 | expect(sample.randomIntegers!.length, equals(3)); 72 | expect(sample.randomIntegers![0], equals(1)); 73 | expect(sample.randomIntegers![1], equals(2)); 74 | expect(sample.randomIntegers![2], equals(3)); 75 | expect(sample.randomDoubles, isNot(isNull)); 76 | expect(sample.randomDoubles!.length, equals(3)); 77 | expect(sample.randomDoubles![0], equals(1.1)); 78 | expect(sample.randomDoubles![1], equals(2.2)); 79 | expect(sample.randomDoubles![2], equals(3.3)); 80 | final pi = sample.personalInfo; 81 | expect(pi, isNot(isNull)); 82 | expect(pi!.firstName, equals('Javier')); 83 | expect(pi.lastName, isNull); 84 | expect(pi.location, isNull); 85 | expect(pi.phones, isNull); 86 | }); 87 | 88 | test("Generated class with private fields should correctly generate JSON", 89 | () { 90 | final phones = []; 91 | final phone = new Phones( 92 | type: "IP", 93 | number: "127.0.0.1", 94 | shouldCall: true, 95 | ); 96 | phones.add(phone); 97 | final personalInfo = new PersonalInfo( 98 | firstName: "User", 99 | lastName: "Test", 100 | location: "In a computer", 101 | phones: phones, 102 | ); 103 | final sample = new Sample( 104 | username: 'Test', 105 | favouriteInteger: 13, 106 | favouriteDouble: 3.1416, 107 | url: 'http://test.test', 108 | htmlUrl: 'http://anothertest.test', 109 | tags: const ['test1'], 110 | randomIntegers: const [4, 5], 111 | randomDoubles: const [4.4, 5.5], 112 | personalInfo: personalInfo, 113 | ); 114 | final codec = new JsonCodec(toEncodable: (dynamic v) => v.toString()); 115 | final encodedJSON = codec.encode(sample.toJson()); 116 | expect(encodedJSON.contains('"username":"Test"'), equals(true)); 117 | expect(encodedJSON.contains('"favouriteInteger":13'), equals(true)); 118 | expect(encodedJSON.contains('"favouriteDouble":3.1416'), equals(true)); 119 | expect(encodedJSON.contains('"url":"http://test.test"'), equals(true)); 120 | expect(encodedJSON.contains('"html_url":"http://anothertest.test"'), 121 | equals(true)); 122 | expect(encodedJSON.contains('"tags":["test1"]'), equals(true)); 123 | expect(encodedJSON.contains('"randomIntegers":[4,5]'), equals(true)); 124 | expect(encodedJSON.contains('"randomDoubles":[4.4,5.5]'), equals(true)); 125 | expect(encodedJSON.contains('"personalInfo":{'), equals(true)); 126 | expect(encodedJSON.contains('"firstName":"User"'), equals(true)); 127 | expect(encodedJSON.contains('"lastName":"Test"'), equals(true)); 128 | expect(encodedJSON.contains('"location":"In a computer"'), equals(true)); 129 | expect(encodedJSON.contains('"phones":['), equals(true)); 130 | expect(encodedJSON.contains('"type":"IP"'), equals(true)); 131 | expect(encodedJSON.contains('"number":"127.0.0.1"'), equals(true)); 132 | expect(encodedJSON.contains('"shouldCall":true'), equals(true)); 133 | }); 134 | 135 | test( 136 | "Generated class with private fields should correctly generate JSON with missing values", 137 | () { 138 | final personalInfo = new PersonalInfo( 139 | firstName: "User", 140 | lastName: null, 141 | ); 142 | final sample = new Sample( 143 | username: 'Test', 144 | favouriteInteger: null, 145 | favouriteDouble: 3.1416, 146 | url: 'http://test.test', 147 | tags: const ['test1'], 148 | randomIntegers: const [4, 5], 149 | randomDoubles: const [4.4, 5.5], 150 | personalInfo: personalInfo, 151 | ); 152 | final codec = new JsonCodec(toEncodable: (dynamic v) => v.toString()); 153 | final encodedJSON = codec.encode(sample.toJson()); 154 | expect(encodedJSON.contains('"username":"Test"'), equals(true)); 155 | expect(encodedJSON.contains('"favouriteInteger":null'), equals(true)); 156 | expect(encodedJSON.contains('"favouriteDouble":3.1416'), equals(true)); 157 | expect(encodedJSON.contains('"url":"http://test.test"'), equals(true)); 158 | expect(encodedJSON.contains('"html_url":null'), equals(true)); 159 | expect(encodedJSON.contains('"tags":["test1"]'), equals(true)); 160 | expect(encodedJSON.contains('"randomIntegers":[4,5]'), equals(true)); 161 | expect(encodedJSON.contains('"randomDoubles":[4.4,5.5]'), equals(true)); 162 | expect(encodedJSON.contains('"personalInfo":{'), equals(true)); 163 | expect(encodedJSON.contains('"firstName":"User"'), equals(true)); 164 | expect(encodedJSON.contains('"lastName":null'), equals(true)); 165 | expect(encodedJSON.contains('"location":null'), equals(true)); 166 | expect(encodedJSON.contains('"phones"'), equals(false)); 167 | }); 168 | }); 169 | } 170 | -------------------------------------------------------------------------------- /lib/helpers.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' as Convert; 2 | import 'dart:math'; 3 | import 'package:json_ast/json_ast.dart' 4 | show Node, ObjectNode, ArrayNode, LiteralNode, PropertyNode; 5 | import 'package:json_to_dart/syntax.dart'; 6 | 7 | const Map PRIMITIVE_TYPES = const { 8 | 'int': true, 9 | 'double': true, 10 | 'String': true, 11 | 'bool': true, 12 | 'DateTime': false, 13 | 'List': false, 14 | 'List': true, 15 | 'List': true, 16 | 'List': true, 17 | 'List': true, 18 | 'Null': true, 19 | }; 20 | 21 | enum ListType { Object, String, Double, Int, Null } 22 | 23 | class MergeableListType { 24 | final ListType listType; 25 | final bool isAmbigous; 26 | 27 | MergeableListType(this.listType, this.isAmbigous); 28 | } 29 | 30 | MergeableListType mergeableListType(List list) { 31 | ListType t = ListType.Null; 32 | bool isAmbigous = false; 33 | list.forEach((e) { 34 | ListType? inferredType = getInferredType(e); 35 | if (t != ListType.Null && t != inferredType) { 36 | isAmbigous = true; 37 | } 38 | t = inferredType ?? ListType.Null; 39 | }); 40 | return MergeableListType(t, isAmbigous); 41 | } 42 | 43 | ListType? getInferredType(dynamic d) { 44 | if (d.runtimeType == int) { 45 | return ListType.Int; 46 | } else if (d.runtimeType == double) { 47 | return ListType.Double; 48 | } else if (d.runtimeType == String) { 49 | return ListType.String; 50 | } else if (d is Map) { 51 | return ListType.Object; 52 | } 53 | return null; 54 | } 55 | 56 | String camelCase(String text) { 57 | String capitalize(Match m) => 58 | m[0]!.substring(0, 1).toUpperCase() + m[0]!.substring(1); 59 | String skip(String s) => ""; 60 | return text.splitMapJoin(new RegExp(r'[a-zA-Z0-9]+'), 61 | onMatch: capitalize, onNonMatch: skip); 62 | } 63 | 64 | String camelCaseFirstLower(String text) { 65 | final camelCaseText = camelCase(text); 66 | final firstChar = camelCaseText.substring(0, 1).toLowerCase(); 67 | final rest = camelCaseText.substring(1); 68 | return '$firstChar$rest'; 69 | } 70 | 71 | decodeJSON(String rawJson) { 72 | return Convert.json.decode(rawJson); 73 | } 74 | 75 | WithWarning mergeObj(Map obj, Map other, String path) { 76 | List warnings = []; 77 | final Map clone = Map.from(obj); 78 | other.forEach((k, v) { 79 | if (clone[k] == null) { 80 | clone[k] = v; 81 | } else { 82 | final String otherType = getTypeName(v); 83 | final String t = getTypeName(clone[k]); 84 | if (t != otherType) { 85 | if (t == 'int' && otherType == 'double') { 86 | // if double was found instead of int, assign the double 87 | clone[k] = v; 88 | } else if (clone[k].runtimeType != 'double' && v.runtimeType != 'int') { 89 | // if types are not equal, then 90 | warnings.add(newAmbiguousType('$path/$k')); 91 | } 92 | } else if (t == 'List') { 93 | List l = List.from(clone[k]); 94 | l.addAll(other[k]); 95 | final mergeableType = mergeableListType(l); 96 | if (ListType.Object == mergeableType.listType) { 97 | WithWarning mergedList = mergeObjectList(l, '$path'); 98 | warnings.addAll(mergedList.warnings); 99 | clone[k] = List.filled(1, mergedList.result); 100 | } else { 101 | if (l.length > 0) { 102 | clone[k] = List.filled(1, l[0]); 103 | } 104 | if (mergeableType.isAmbigous) { 105 | warnings.add(newAmbiguousType('$path/$k')); 106 | } 107 | } 108 | } else if (t == 'Class') { 109 | WithWarning mergedObj = mergeObj(clone[k], other[k], '$path/$k'); 110 | warnings.addAll(mergedObj.warnings); 111 | clone[k] = mergedObj.result; 112 | } 113 | } 114 | }); 115 | return new WithWarning(clone, warnings); 116 | } 117 | 118 | WithWarning mergeObjectList(List list, String path, 119 | [int idx = -1]) { 120 | List warnings = []; 121 | Map obj = new Map(); 122 | for (var i = 0; i < list.length; i++) { 123 | final toMerge = list[i]; 124 | if (toMerge is Map) { 125 | toMerge.forEach((k, v) { 126 | final String t = getTypeName(obj[k]); 127 | if (obj[k] == null) { 128 | obj[k] = v; 129 | } else { 130 | final String otherType = getTypeName(v); 131 | if (t != otherType) { 132 | if (t == 'int' && otherType == 'double') { 133 | // if double was found instead of int, assign the double 134 | obj[k] = v; 135 | } else if (t != 'double' && otherType != 'int') { 136 | // if types are not equal, then 137 | int realIndex = i; 138 | if (idx != -1) { 139 | realIndex = idx - i; 140 | } 141 | final String ambiguosTypePath = '$path[$realIndex]/$k'; 142 | warnings.add(newAmbiguousType(ambiguosTypePath)); 143 | } 144 | } else if (t == 'List') { 145 | List l = List.from(obj[k]); 146 | final int beginIndex = l.length; 147 | l.addAll(v); 148 | // bug is here 149 | final mergeableType = mergeableListType(l); 150 | if (ListType.Object == mergeableType.listType) { 151 | WithWarning mergedList = 152 | mergeObjectList(l, '$path[$i]/$k', beginIndex); 153 | warnings.addAll(mergedList.warnings); 154 | obj[k] = List.filled(1, mergedList.result); 155 | } else { 156 | if (l.length > 0) { 157 | obj[k] = List.filled(1, l[0]); 158 | } 159 | if (mergeableType.isAmbigous) { 160 | warnings.add(newAmbiguousType('$path[$i]/$k')); 161 | } 162 | } 163 | } else if (t == 'Class') { 164 | int properIndex = i; 165 | if (idx != -1) { 166 | properIndex = i - idx; 167 | } 168 | WithWarning mergedObj = mergeObj( 169 | obj[k], 170 | v, 171 | '$path[$properIndex]/$k', 172 | ); 173 | warnings.addAll(mergedObj.warnings); 174 | obj[k] = mergedObj.result; 175 | } 176 | } 177 | }); 178 | } 179 | } 180 | return new WithWarning(obj, warnings); 181 | } 182 | 183 | isPrimitiveType(String typeName) { 184 | final isPrimitive = PRIMITIVE_TYPES[typeName]; 185 | if (isPrimitive == null) { 186 | return false; 187 | } 188 | return isPrimitive; 189 | } 190 | 191 | String fixFieldName(String name, 192 | {required TypeDefinition typeDef, bool privateField = false}) { 193 | var properName = name; 194 | if (name.startsWith('_') || name.startsWith(new RegExp(r'[0-9]'))) { 195 | final firstCharType = typeDef.name.substring(0, 1).toLowerCase(); 196 | properName = '$firstCharType$name'; 197 | } 198 | final fieldName = camelCaseFirstLower(properName); 199 | if (privateField) { 200 | return '_$fieldName'; 201 | } 202 | return fieldName; 203 | } 204 | 205 | String getTypeName(dynamic obj) { 206 | if (obj is String) { 207 | return 'String'; 208 | } else if (obj is int) { 209 | return 'int'; 210 | } else if (obj is double) { 211 | return 'double'; 212 | } else if (obj is bool) { 213 | return 'bool'; 214 | } else if (obj == null) { 215 | return 'Null'; 216 | } else if (obj is List) { 217 | return 'List'; 218 | } else { 219 | // assumed class 220 | return 'Class'; 221 | } 222 | } 223 | 224 | Node? navigateNode(Node? astNode, String path) { 225 | Node? node; 226 | if (astNode is ObjectNode) { 227 | final ObjectNode objectNode = astNode; 228 | PropertyNode? propertyNode; 229 | for (int i = 0; i < objectNode.children.length; i++) { 230 | final prop = objectNode.children[i]; 231 | if (prop.key != null && prop.key?.value == path) { 232 | propertyNode = prop; 233 | break; 234 | } 235 | } 236 | if (propertyNode != null) { 237 | node = propertyNode.value; 238 | } 239 | } 240 | if (astNode is ArrayNode) { 241 | final ArrayNode arrayNode = astNode; 242 | final index = int.tryParse(path) ?? null; 243 | if (index != null && arrayNode.children.length > index) { 244 | node = arrayNode.children[index]; 245 | } 246 | } 247 | return node; 248 | } 249 | 250 | final _pattern = RegExp(r"([0-9]+)\.{0,1}([0-9]*)e(([-0-9]+))"); 251 | 252 | bool isASTLiteralDouble(Node? astNode) { 253 | if (astNode != null && astNode is LiteralNode) { 254 | final LiteralNode literalNode = astNode; 255 | if (literalNode.raw != null) { 256 | final containsPoint = literalNode.raw!.contains('.'); 257 | final containsExponent = literalNode.raw!.contains('e'); 258 | if (containsPoint || containsExponent) { 259 | var isDouble = containsPoint; 260 | if (containsExponent) { 261 | final matches = _pattern.firstMatch(literalNode.raw!); 262 | if (matches != null) { 263 | final integer = matches[1]!; 264 | final comma = matches[2]!; 265 | final exponent = matches[3]!; 266 | isDouble = _isDoubleWithExponential(integer, comma, exponent); 267 | } 268 | } 269 | return isDouble; 270 | } 271 | } 272 | } 273 | return false; 274 | } 275 | 276 | bool _isDoubleWithExponential(String integer, String comma, String exponent) { 277 | final integerNumber = int.tryParse(integer) ?? 0; 278 | final exponentNumber = int.tryParse(exponent) ?? 0; 279 | final commaNumber = int.tryParse(comma) ?? 0; 280 | if (exponentNumber == 0) { 281 | return commaNumber > 0; 282 | } 283 | if (exponentNumber > 0) { 284 | return exponentNumber < comma.length && commaNumber > 0; 285 | } 286 | return commaNumber > 0 || 287 | ((integerNumber.toDouble() * pow(10, exponentNumber)).remainder(1) > 0); 288 | } 289 | -------------------------------------------------------------------------------- /lib/syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_ast/json_ast.dart' show Node; 2 | import 'package:json_to_dart/helpers.dart'; 3 | 4 | const String emptyListWarn = "list is empty"; 5 | const String ambiguousListWarn = "list is ambiguous"; 6 | const String ambiguousTypeWarn = "type is ambiguous"; 7 | 8 | class Warning { 9 | final String warning; 10 | final String path; 11 | 12 | Warning(this.warning, this.path); 13 | } 14 | 15 | Warning newEmptyListWarn(String path) { 16 | return new Warning(emptyListWarn, path); 17 | } 18 | 19 | Warning newAmbiguousListWarn(String path) { 20 | return new Warning(ambiguousListWarn, path); 21 | } 22 | 23 | Warning newAmbiguousType(String path) { 24 | return new Warning(ambiguousTypeWarn, path); 25 | } 26 | 27 | class WithWarning { 28 | final T result; 29 | final List warnings; 30 | 31 | WithWarning(this.result, this.warnings); 32 | } 33 | 34 | class TypeDefinition { 35 | String name; 36 | String? subtype; 37 | bool isAmbiguous = false; 38 | bool _isPrimitive = false; 39 | 40 | factory TypeDefinition.fromDynamic(dynamic obj, Node? astNode) { 41 | bool isAmbiguous = false; 42 | final type = getTypeName(obj); 43 | if (type == 'List') { 44 | List list = obj; 45 | String elemType; 46 | if (list.length > 0) { 47 | elemType = getTypeName(list[0]); 48 | for (dynamic listVal in list) { 49 | final typeName = getTypeName(listVal); 50 | if (elemType != typeName) { 51 | isAmbiguous = true; 52 | break; 53 | } 54 | } 55 | } else { 56 | // when array is empty insert Null just to warn the user 57 | elemType = "Null"; 58 | } 59 | return new TypeDefinition(type, 60 | astNode: astNode, subtype: elemType, isAmbiguous: isAmbiguous); 61 | } 62 | return new TypeDefinition(type, astNode: astNode, isAmbiguous: isAmbiguous); 63 | } 64 | 65 | TypeDefinition(this.name, 66 | {this.subtype, this.isAmbiguous = false, Node? astNode}) { 67 | if (subtype == null) { 68 | _isPrimitive = isPrimitiveType(this.name); 69 | if (this.name == 'int' && isASTLiteralDouble(astNode)) { 70 | this.name = 'double'; 71 | } 72 | } else { 73 | _isPrimitive = isPrimitiveType('$name<$subtype>'); 74 | } 75 | } 76 | 77 | bool operator ==(other) { 78 | if (other is TypeDefinition) { 79 | TypeDefinition otherTypeDef = other; 80 | return this.name == otherTypeDef.name && 81 | this.subtype == otherTypeDef.subtype && 82 | this.isAmbiguous == otherTypeDef.isAmbiguous && 83 | this._isPrimitive == otherTypeDef._isPrimitive; 84 | } 85 | return false; 86 | } 87 | 88 | bool get isPrimitive => _isPrimitive; 89 | 90 | bool get isPrimitiveList => _isPrimitive && name == 'List'; 91 | 92 | String _buildParseClass(String expression) { 93 | final properType = subtype != null ? subtype : name; 94 | return 'new $properType.fromJson($expression)'; 95 | } 96 | 97 | String _buildToJsonClass(String expression, [bool nullGuard = true]) { 98 | if (nullGuard) { 99 | return '$expression!.toJson()'; 100 | } 101 | return '$expression.toJson()'; 102 | } 103 | 104 | String jsonParseExpression(String key, bool privateField) { 105 | final jsonKey = "json['$key']"; 106 | final fieldKey = 107 | fixFieldName(key, typeDef: this, privateField: privateField); 108 | if (isPrimitive) { 109 | if (name == "List") { 110 | return "$fieldKey = json['$key'].cast<$subtype>();"; 111 | } 112 | return "$fieldKey = json['$key'];"; 113 | } else if (name == "List" && subtype == "DateTime") { 114 | return "$fieldKey = json['$key'].map((v) => DateTime.tryParse(v));"; 115 | } else if (name == "DateTime") { 116 | return "$fieldKey = DateTime.tryParse(json['$key']);"; 117 | } else if (name == 'List') { 118 | // list of class 119 | return "if (json['$key'] != null) {\n\t\t\t$fieldKey = <$subtype>[];\n\t\t\tjson['$key'].forEach((v) { $fieldKey!.add(new $subtype.fromJson(v)); });\n\t\t}"; 120 | } else { 121 | // class 122 | return "$fieldKey = json['$key'] != null ? ${_buildParseClass(jsonKey)} : null;"; 123 | } 124 | } 125 | 126 | String toJsonExpression(String key, bool privateField) { 127 | final fieldKey = 128 | fixFieldName(key, typeDef: this, privateField: privateField); 129 | final thisKey = 'this.$fieldKey'; 130 | if (isPrimitive) { 131 | return "data['$key'] = $thisKey;"; 132 | } else if (name == 'List') { 133 | // class list 134 | return """if ($thisKey != null) { 135 | data['$key'] = $thisKey!.map((v) => ${_buildToJsonClass('v', false)}).toList(); 136 | }"""; 137 | } else { 138 | // class 139 | return """if ($thisKey != null) { 140 | data['$key'] = ${_buildToJsonClass(thisKey)}; 141 | }"""; 142 | } 143 | } 144 | } 145 | 146 | class Dependency { 147 | String name; 148 | final TypeDefinition typeDef; 149 | 150 | Dependency(this.name, this.typeDef); 151 | 152 | String get className => camelCase(name); 153 | } 154 | 155 | class ClassDefinition { 156 | final String _name; 157 | final bool _privateFields; 158 | final Map fields = new Map(); 159 | 160 | String get name => _name; 161 | bool get privateFields => _privateFields; 162 | 163 | List get dependencies { 164 | final dependenciesList = []; 165 | final keys = fields.keys; 166 | keys.forEach((k) { 167 | final f = fields[k]; 168 | if (f != null && !f.isPrimitive) { 169 | dependenciesList.add(new Dependency(k, f)); 170 | } 171 | }); 172 | return dependenciesList; 173 | } 174 | 175 | ClassDefinition(this._name, [this._privateFields = false]); 176 | 177 | bool operator ==(other) { 178 | if (other is ClassDefinition) { 179 | ClassDefinition otherClassDef = other; 180 | return this.isSubsetOf(otherClassDef) && otherClassDef.isSubsetOf(this); 181 | } 182 | return false; 183 | } 184 | 185 | bool isSubsetOf(ClassDefinition other) { 186 | final List keys = this.fields.keys.toList(); 187 | final int len = keys.length; 188 | for (int i = 0; i < len; i++) { 189 | TypeDefinition? otherTypeDef = other.fields[keys[i]]; 190 | if (otherTypeDef != null) { 191 | TypeDefinition? typeDef = this.fields[keys[i]]; 192 | if (typeDef != otherTypeDef) { 193 | return false; 194 | } 195 | } else { 196 | return false; 197 | } 198 | } 199 | return true; 200 | } 201 | 202 | hasField(TypeDefinition otherField) { 203 | final key = fields.keys 204 | .firstWhere((k) => fields[k] == otherField, orElse: () => ""); 205 | return key != ""; 206 | } 207 | 208 | addField(String name, TypeDefinition typeDef) { 209 | fields[name] = typeDef; 210 | } 211 | 212 | void _addTypeDef(TypeDefinition typeDef, StringBuffer sb) { 213 | sb.write('${typeDef.name}'); 214 | if (typeDef.subtype != null) { 215 | sb.write('<${typeDef.subtype}>'); 216 | } 217 | } 218 | 219 | String get _fieldList { 220 | return fields.keys.map((key) { 221 | final f = fields[key]!; 222 | final fieldName = 223 | fixFieldName(key, typeDef: f, privateField: privateFields); 224 | final sb = new StringBuffer(); 225 | sb.write('\t'); 226 | _addTypeDef(f, sb); 227 | sb.write('? $fieldName;'); 228 | return sb.toString(); 229 | }).join('\n'); 230 | } 231 | 232 | String get _gettersSetters { 233 | return fields.keys.map((key) { 234 | final f = fields[key]!; 235 | final publicFieldName = 236 | fixFieldName(key, typeDef: f, privateField: false); 237 | final privateFieldName = 238 | fixFieldName(key, typeDef: f, privateField: true); 239 | final sb = new StringBuffer(); 240 | sb.write('\t'); 241 | _addTypeDef(f, sb); 242 | sb.write( 243 | '? get $publicFieldName => $privateFieldName;\n\tset $publicFieldName('); 244 | _addTypeDef(f, sb); 245 | sb.write('? $publicFieldName) => $privateFieldName = $publicFieldName;'); 246 | return sb.toString(); 247 | }).join('\n'); 248 | } 249 | 250 | String get _defaultPrivateConstructor { 251 | final sb = new StringBuffer(); 252 | sb.write('\t$name({'); 253 | var i = 0; 254 | var len = fields.keys.length - 1; 255 | fields.keys.forEach((key) { 256 | final f = fields[key]!; 257 | final publicFieldName = 258 | fixFieldName(key, typeDef: f, privateField: false); 259 | _addTypeDef(f, sb); 260 | sb.write('? $publicFieldName'); 261 | if (i != len) { 262 | sb.write(', '); 263 | } 264 | i++; 265 | }); 266 | sb.write('}) {\n'); 267 | fields.keys.forEach((key) { 268 | final f = fields[key]!; 269 | final publicFieldName = 270 | fixFieldName(key, typeDef: f, privateField: false); 271 | final privateFieldName = 272 | fixFieldName(key, typeDef: f, privateField: true); 273 | sb.write('if ($publicFieldName != null) {\n'); 274 | sb.write('this.$privateFieldName = $publicFieldName;\n'); 275 | sb.write('}\n'); 276 | }); 277 | sb.write('}'); 278 | return sb.toString(); 279 | } 280 | 281 | String get _defaultConstructor { 282 | final sb = new StringBuffer(); 283 | sb.write('\t$name({'); 284 | var i = 0; 285 | var len = fields.keys.length - 1; 286 | fields.keys.forEach((key) { 287 | final f = fields[key]!; 288 | final fieldName = 289 | fixFieldName(key, typeDef: f, privateField: privateFields); 290 | sb.write('this.$fieldName'); 291 | if (i != len) { 292 | sb.write(', '); 293 | } 294 | i++; 295 | }); 296 | sb.write('});'); 297 | return sb.toString(); 298 | } 299 | 300 | String get _jsonParseFunc { 301 | final sb = new StringBuffer(); 302 | sb.write('\t$name'); 303 | sb.write('.fromJson(Map json) {\n'); 304 | fields.keys.forEach((k) { 305 | sb.write('\t\t${fields[k]!.jsonParseExpression(k, privateFields)}\n'); 306 | }); 307 | sb.write('\t}'); 308 | return sb.toString(); 309 | } 310 | 311 | String get _jsonGenFunc { 312 | final sb = new StringBuffer(); 313 | sb.write( 314 | '\tMap toJson() {\n\t\tfinal Map data = new Map();\n'); 315 | fields.keys.forEach((k) { 316 | sb.write('\t\t${fields[k]!.toJsonExpression(k, privateFields)}\n'); 317 | }); 318 | sb.write('\t\treturn data;\n'); 319 | sb.write('\t}'); 320 | return sb.toString(); 321 | } 322 | 323 | String toString() { 324 | if (privateFields) { 325 | return 'class $name {\n$_fieldList\n\n$_defaultPrivateConstructor\n\n$_gettersSetters\n\n$_jsonParseFunc\n\n$_jsonGenFunc\n}\n'; 326 | } else { 327 | return 'class $name {\n$_fieldList\n\n$_defaultConstructor\n\n$_jsonParseFunc\n\n$_jsonGenFunc\n}\n'; 328 | } 329 | } 330 | } 331 | --------------------------------------------------------------------------------