├── .DS_Store ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── build.sh ├── json2dartc.dart └── json_to_dart │ ├── helpers.dart │ ├── json_to_dart.dart │ ├── model_generator.dart │ ├── syntax.dart │ └── warning.dart ├── build ├── json2dart └── json2dart.exe ├── media └── example.gif ├── pubspec.lock └── pubspec.yaml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacifio/json2dart/eb09c60dbf51df1474292148f9852cc7e0d249e7/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | doc/api/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial version, created by Stagehand 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Adib Mohsin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Json 2 Dart Command line utility 2 | 3 | ## Important note 4 | 5 | There is already a package called `json2dart` so this package will be called `json2dartc` ! 6 | 7 | 8 | 9 | This project was made using [javiercbk's json_to_dart package](https://github.com/javiercbk/json_to_dart) ! This CLI was made to directly convert JSON stuctures into Dart classes . I personally don't like build runners and json serializers so I made this for my workflow . Feel free to open issues and submit PRs . 10 | 11 | ## How to use 12 | 13 | Install this via `pub` 14 | 15 | `pub global activate json2dartc` 16 | 17 | ## Example 18 | 19 | `json2dartc -u https://reqres.in/api/users -m get -e data -n Example` 20 | 21 | ## Null safety 22 | 23 | To turn on null safe code generation , add the flag `--null-safe` , Example : 24 | 25 | `json2dartc -u https://reqres.in/api/users -m get -n Example --null-safe` 26 | 27 | ## Options 28 | 29 | ``` 30 | -u, --api API Endpoint required to grab the json from 31 | -e, --entry Entry point for json data structure , e.g data.data will get the nested data array/object from API response 32 | -n, --name Name of your data class 33 | (defaults to "AutoGenerated") 34 | -h, --headers Headers for your API endpoint 35 | -m, --method Method for http request , defaults to GET 36 | (defaults to "GET") 37 | ``` 38 | 39 | | Option | required | default | example | note | 40 | | ------ | -------- | --------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------- | 41 | | -u | true | required field | https://reqres.in/api/users | | 42 | | -e | false | '' | data | it's used for special access in json data , e.g data.data will access nested data object within data object | 43 | | -n | false | 'AutoGenerated' | Example | | 44 | | -h | false | {} | access-token=01234,foo=bar | | 45 | | -m | false | GET | GET/POST | | 46 | 47 | ## Upcoming plans 48 | 49 | - [x] Null safety support 50 | - [ ] Tool itself written with null safety 51 | - [ ] Private memmbers option 52 | - [ ] Option to load json from a file 53 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | dart compile exe ./json2dartc.dart -o ../build/json2dartc && dart compile exe ./json2dartc.dart -o ../build/json2dartc.exe -------------------------------------------------------------------------------- /bin/json2dartc.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Adib Mohsin 2 | // Credits to https://github.com/javiercbk for making `json_to_dart` package 3 | 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | import 'package:args/args.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | import 'json_to_dart/json_to_dart.dart'; 10 | 11 | void quit(String msg) { 12 | print(msg ?? 'Something went wrong'); 13 | exit(0); 14 | } 15 | 16 | Map parseHeaders(String headers) { 17 | try { 18 | final result = {}; 19 | final split = headers.split(','); 20 | split.forEach((header) { 21 | final data = header.split('='); 22 | 23 | result[data[0]] = data[1]; 24 | }); 25 | 26 | return result; 27 | } catch (_) { 28 | return {}; 29 | } 30 | } 31 | 32 | void main(List arguments) async { 33 | final stopwatch = Stopwatch()..start(); 34 | final parser = ArgParser(); 35 | 36 | parser.addOption('api', 37 | help: 'API Endpoint required to grab the json from', abbr: 'u'); 38 | parser.addOption('entry', 39 | help: 40 | 'Entry point for json data structure , e.g data.data will get the nested data array/object from API response', 41 | abbr: 'e'); 42 | parser.addOption('name', 43 | help: 'Name of your data class', abbr: 'n', defaultsTo: 'AutoGenerated'); 44 | parser.addOption('headers', help: 'Headers for your API endpoint', abbr: 'h'); 45 | parser.addOption('method', 46 | help: 'Method for http request , defaults to GET', 47 | abbr: 'm', 48 | defaultsTo: 'GET'); 49 | parser.addFlag('null-safe', 50 | negatable: false, 51 | defaultsTo: false, 52 | help: 53 | 'Add this flag if you want to generate null safe code , by default we will generate without null safety !'); 54 | 55 | try { 56 | final results = parser.parse(arguments); 57 | 58 | if (results['null-safe']) { 59 | print('You have turned on null safety !'); 60 | } 61 | 62 | if (results['api'] != null) { 63 | try { 64 | String body; 65 | int statusCode; 66 | 67 | try { 68 | final url = Uri.parse(results['api']); 69 | final header = parseHeaders(results['headers']); 70 | 71 | if (results['method'].toString().trim().toLowerCase() == 'get') { 72 | final response = 73 | await http.get(url, headers: Map.from(header)); 74 | body = response.body; 75 | statusCode = response.statusCode; 76 | } else if (results['method'].toString().trim().toLowerCase() == 77 | 'post') { 78 | final response = 79 | await http.post(url, headers: Map.from(header)); 80 | body = response.body; 81 | statusCode = response.statusCode; 82 | } else { 83 | quit('Only get and post methods are supported'); 84 | } 85 | } catch (e) { 86 | quit('Could not send request , quitting !'); 87 | } 88 | 89 | if (statusCode != null) { 90 | if (statusCode == 200) { 91 | try { 92 | final parsed = jsonDecode(body); 93 | 94 | dynamic finalData; 95 | 96 | if (results['entry'] != null) { 97 | try { 98 | final split = results['entry'].toString().trim().split('.'); 99 | 100 | if (split.length == 1) { 101 | finalData = parsed[split[0]]; 102 | } else { 103 | finalData = parsed; 104 | split.forEach((element) { 105 | finalData = finalData[element]; 106 | }); 107 | } 108 | } catch (_) { 109 | print('Entry is not valid , using default data'); 110 | finalData = parsed; 111 | } 112 | } else { 113 | finalData = parsed; 114 | } 115 | 116 | if (finalData != null) { 117 | final name = results['name']; 118 | final encoder = JsonEncoder.withIndent(' '); 119 | 120 | print('FILTERED JSON RESPONSE \n'); 121 | print(encoder.convert(finalData)); 122 | 123 | try { 124 | final stringify = jsonEncode(finalData); 125 | final classGenerator = ModelGenerator(name); 126 | final dartCode = classGenerator.generateDartClasses( 127 | stringify, 128 | nullSafe: results['null-safe'], 129 | ); 130 | 131 | try { 132 | var file = File('$name.dart'); 133 | 134 | file.writeAsString(dartCode.code).then((_) { 135 | print('Model created successfully\n'); 136 | print( 137 | 'Took ${stopwatch.elapsed.inMilliseconds / 1000} seconds !'); 138 | }); 139 | } catch (_) { 140 | quit('Could not write to file'); 141 | } 142 | } catch (e) { 143 | print("\n"); 144 | print(e); 145 | quit('Could not generate models !'); 146 | } 147 | } else { 148 | quit('Something went wrong , could not parse JSON'); 149 | } 150 | } catch (_) { 151 | quit('Error parsing JSON body , quitting !'); 152 | } 153 | } else if (statusCode == 400) { 154 | quit('Server not found , quitting !'); 155 | } else if (statusCode == 401) { 156 | quit('Authorization error , consider providing an access token'); 157 | } else { 158 | quit('Server didn\'nt respond , status code $statusCode'); 159 | } 160 | } 161 | } on Exception catch (_) { 162 | quit('Could not parse API endpoint , quitting !'); 163 | } 164 | } else { 165 | quit('API is an empty string , quitting !'); 166 | } 167 | } on FormatException catch (_) { 168 | print(parser.usage); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /bin/json_to_dart/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; 5 | 6 | import './syntax.dart'; 7 | 8 | const Map PRIMITIVE_TYPES = const { 9 | 'int': true, 10 | 'double': true, 11 | 'String': true, 12 | 'bool': true, 13 | 'DateTime': false, 14 | 'List': false, 15 | 'List': true, 16 | 'List': true, 17 | 'List': true, 18 | 'List': true, 19 | 'Null': true, 20 | }; 21 | 22 | enum ListType { Object, String, Double, Int, Null } 23 | 24 | class MergeableListType { 25 | final ListType listType; 26 | final bool isAmbigous; 27 | 28 | MergeableListType(this.listType, this.isAmbigous); 29 | } 30 | 31 | MergeableListType mergeableListType(List list) { 32 | ListType t = ListType.Null; 33 | bool isAmbigous = false; 34 | list.forEach((e) { 35 | ListType inferredType; 36 | if (e.runtimeType == 'int') { 37 | inferredType = ListType.Int; 38 | } else if (e.runtimeType == 'double') { 39 | inferredType = ListType.Double; 40 | } else if (e.runtimeType == 'string') { 41 | inferredType = ListType.String; 42 | } else if (e is Map) { 43 | inferredType = ListType.Object; 44 | } 45 | if (t != ListType.Null && t != inferredType) { 46 | isAmbigous = true; 47 | } 48 | t = inferredType; 49 | }); 50 | return MergeableListType(t, isAmbigous); 51 | } 52 | 53 | String camelCase(String text) { 54 | String capitalize(Match m) => 55 | m[0].substring(0, 1).toUpperCase() + m[0].substring(1); 56 | String skip(String s) => ""; 57 | return text.splitMapJoin(RegExp(r'[a-zA-Z0-9]+'), 58 | onMatch: capitalize, onNonMatch: skip); 59 | } 60 | 61 | String camelCaseFirstLower(String text) { 62 | final camelCaseText = camelCase(text); 63 | final firstChar = camelCaseText.substring(0, 1).toLowerCase(); 64 | final rest = camelCaseText.substring(1); 65 | return '$firstChar$rest'; 66 | } 67 | 68 | decodeJSON(String rawJson) { 69 | return Convert.json.decode(rawJson); 70 | } 71 | 72 | WithWarning mergeObj(Map obj, Map other, String path) { 73 | List warnings = []; 74 | final Map clone = Map.from(obj); 75 | other.forEach((k, v) { 76 | if (clone[k] == null) { 77 | clone[k] = v; 78 | } else { 79 | final String otherType = getTypeName(v); 80 | final String t = getTypeName(clone[k]); 81 | if (t != otherType) { 82 | if (t == 'int' && otherType == 'double') { 83 | // if double was found instead of int, assign the double 84 | clone[k] = v; 85 | } else if (clone[k].runtimeType != 'double' && v.runtimeType != 'int') { 86 | // if types are not equal, then 87 | warnings.add(newAmbiguousType('$path/$k')); 88 | } 89 | } else if (t == 'List') { 90 | List l = List.from(clone[k]); 91 | l.addAll(other[k]); 92 | final mergeableType = mergeableListType(l); 93 | if (ListType.Object == mergeableType.listType) { 94 | WithWarning mergedList = mergeObjectList(l, '$path'); 95 | warnings.addAll(mergedList.warnings); 96 | clone[k] = List.filled(1, mergedList.result); 97 | } else { 98 | if (l.length > 0) { 99 | clone[k] = List.filled(1, l[0]); 100 | } 101 | if (mergeableType.isAmbigous) { 102 | warnings.add(newAmbiguousType('$path/$k')); 103 | } 104 | } 105 | } else if (t == 'Class') { 106 | WithWarning mergedObj = mergeObj(clone[k], other[k], '$path/$k'); 107 | warnings.addAll(mergedObj.warnings); 108 | clone[k] = mergedObj.result; 109 | } 110 | } 111 | }); 112 | return WithWarning(clone, warnings); 113 | } 114 | 115 | WithWarning mergeObjectList(List list, String path, 116 | [int idx = -1]) { 117 | List warnings = []; 118 | Map obj = Map(); 119 | for (var i = 0; i < list.length; i++) { 120 | final toMerge = list[i]; 121 | if (toMerge is Map) { 122 | toMerge.forEach((k, v) { 123 | final String t = getTypeName(obj[k]); 124 | if (obj[k] == null) { 125 | obj[k] = v; 126 | } else { 127 | final String otherType = getTypeName(v); 128 | if (t != otherType) { 129 | if (t == 'int' && otherType == 'double') { 130 | // if double was found instead of int, assign the double 131 | obj[k] = v; 132 | } else if (t != 'double' && otherType != 'int') { 133 | // if types are not equal, then 134 | int realIndex = i; 135 | if (idx != -1) { 136 | realIndex = idx - i; 137 | } 138 | final String ambiguosTypePath = '$path[$realIndex]/$k'; 139 | warnings.add(newAmbiguousType(ambiguosTypePath)); 140 | } 141 | } else if (t == 'List') { 142 | List l = List.from(obj[k]); 143 | final int beginIndex = l.length; 144 | l.addAll(v); 145 | // bug is here 146 | final mergeableType = mergeableListType(l); 147 | if (ListType.Object == mergeableType.listType) { 148 | WithWarning mergedList = 149 | mergeObjectList(l, '$path[$i]/$k', beginIndex); 150 | warnings.addAll(mergedList.warnings); 151 | obj[k] = List.filled(1, mergedList.result); 152 | } else { 153 | if (l.length > 0) { 154 | obj[k] = List.filled(1, l[0]); 155 | } 156 | if (mergeableType.isAmbigous) { 157 | warnings.add(newAmbiguousType('$path[$i]/$k')); 158 | } 159 | } 160 | } else if (t == 'Class') { 161 | int properIndex = i; 162 | if (idx != -1) { 163 | properIndex = i - idx; 164 | } 165 | WithWarning mergedObj = mergeObj( 166 | obj[k], 167 | v, 168 | '$path[$properIndex]/$k', 169 | ); 170 | warnings.addAll(mergedObj.warnings); 171 | obj[k] = mergedObj.result; 172 | } 173 | } 174 | }); 175 | } 176 | } 177 | return WithWarning(obj, warnings); 178 | } 179 | 180 | isPrimitiveType(String typeName) { 181 | final isPrimitive = PRIMITIVE_TYPES[typeName]; 182 | if (isPrimitive == null) { 183 | return false; 184 | } 185 | return isPrimitive; 186 | } 187 | 188 | String fixFieldName(String name, 189 | {TypeDefinition typeDef, bool privateField = false}) { 190 | var properName = name; 191 | if (name.startsWith('_') || name.startsWith(RegExp(r'[0-9]'))) { 192 | final firstCharType = typeDef.name.substring(0, 1).toLowerCase(); 193 | properName = '$firstCharType$name'; 194 | } 195 | final fieldName = camelCaseFirstLower(properName); 196 | if (privateField) { 197 | return '_$fieldName'; 198 | } 199 | return fieldName; 200 | } 201 | 202 | String getTypeName(dynamic obj) { 203 | if (obj is String) { 204 | return 'String'; 205 | } else if (obj is int) { 206 | return 'int'; 207 | } else if (obj is double) { 208 | return 'double'; 209 | } else if (obj is bool) { 210 | return 'bool'; 211 | } else if (obj == null) { 212 | return 'Null'; 213 | } else if (obj is List) { 214 | return 'List'; 215 | } else { 216 | // assumed class 217 | return 'Class'; 218 | } 219 | } 220 | 221 | Node navigateNode(Node astNode, String path) { 222 | Node node; 223 | if (astNode is ObjectNode) { 224 | final ObjectNode objectNode = astNode; 225 | final propertyNode = objectNode.children.firstWhere((final prop) { 226 | return prop.key.value == path; 227 | }, orElse: () { 228 | return null; 229 | }); 230 | if (propertyNode != null) { 231 | node = propertyNode.value; 232 | } 233 | } 234 | if (astNode is ArrayNode) { 235 | final ArrayNode arrayNode = astNode; 236 | final index = int.tryParse(path) ?? null; 237 | if (index != null && arrayNode.children.length > index) { 238 | node = arrayNode.children[index]; 239 | } 240 | } 241 | return node; 242 | } 243 | 244 | final _pattern = RegExp(r"([0-9]+)\.{0,1}([0-9]*)e(([-0-9]+))"); 245 | 246 | bool isASTLiteralDouble(Node astNode) { 247 | if (astNode != null && astNode is LiteralNode) { 248 | final LiteralNode literalNode = astNode; 249 | final containsPoint = literalNode.raw.contains('.'); 250 | final containsExponent = literalNode.raw.contains('e'); 251 | if (containsPoint || containsExponent) { 252 | var isDouble = containsPoint; 253 | if (containsExponent) { 254 | final matches = _pattern.firstMatch(literalNode.raw); 255 | if (matches != null) { 256 | final integer = matches[1]; 257 | final comma = matches[2]; 258 | final exponent = matches[3]; 259 | isDouble = _isDoubleWithExponential(integer, comma, exponent); 260 | } 261 | } 262 | return isDouble; 263 | } 264 | } 265 | return false; 266 | } 267 | 268 | bool _isDoubleWithExponential(String integer, String comma, String exponent) { 269 | final integerNumber = int.tryParse(integer) ?? 0; 270 | final exponentNumber = int.tryParse(exponent) ?? 0; 271 | final commaNumber = int.tryParse(comma) ?? 0; 272 | if (exponentNumber != null) { 273 | if (exponentNumber == 0) { 274 | return commaNumber > 0; 275 | } 276 | if (exponentNumber > 0) { 277 | return exponentNumber < comma.length && commaNumber > 0; 278 | } 279 | return commaNumber > 0 || 280 | ((integerNumber.toDouble() * pow(10, exponentNumber)).remainder(1) > 0); 281 | } 282 | return false; 283 | } 284 | -------------------------------------------------------------------------------- /bin/json_to_dart/json_to_dart.dart: -------------------------------------------------------------------------------- 1 | export './model_generator.dart'; 2 | -------------------------------------------------------------------------------- /bin/json_to_dart/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 'helpers.dart'; 6 | import '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 = HashMap(); 27 | 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 | return this.hints.firstWhere((h) => h.path == path, orElse: () => null); 39 | } 40 | 41 | List _generateClassDefinition(String className, 42 | dynamic jsonRawDynamicData, String path, Node astNode, bool nullSafe) { 43 | List warnings = []; 44 | if (jsonRawDynamicData is List) { 45 | // if first element is an array, start in the first element. 46 | final node = navigateNode(astNode, '0'); 47 | _generateClassDefinition( 48 | className, jsonRawDynamicData[0], path, node, nullSafe); 49 | } else { 50 | final Map jsonRawData = jsonRawDynamicData; 51 | final keys = jsonRawData.keys; 52 | ClassDefinition classDefinition = 53 | ClassDefinition(className, _privateFields, nullSafe); 54 | keys.forEach((key) { 55 | TypeDefinition typeDef; 56 | final hint = _hintForPath('$path/$key'); 57 | final node = navigateNode(astNode, key); 58 | if (hint != null) { 59 | typeDef = TypeDefinition( 60 | hint.type, 61 | nullSafe, 62 | astNode: node, 63 | ); 64 | } else { 65 | typeDef = 66 | TypeDefinition.fromDynamic(jsonRawData[key], node, nullSafe); 67 | } 68 | if (typeDef.name == 'Class') { 69 | typeDef.name = camelCase(key); 70 | } 71 | if (typeDef.name == 'List' && typeDef.subtype == 'Null') { 72 | warnings.add(newEmptyListWarn('$path/$key')); 73 | } 74 | if (typeDef.subtype != null && typeDef.subtype == 'Class') { 75 | typeDef.subtype = camelCase(key); 76 | } 77 | if (typeDef.isAmbiguous) { 78 | warnings.add(newAmbiguousListWarn('$path/$key')); 79 | } 80 | classDefinition.addField(key, typeDef); 81 | }); 82 | final similarClass = allClasses.firstWhere((cd) => cd == classDefinition, 83 | orElse: () => null); 84 | if (similarClass != null) { 85 | final similarClassName = similarClass.name; 86 | final currentClassName = classDefinition.name; 87 | sameClassMapping[currentClassName] = similarClassName; 88 | } else { 89 | allClasses.add(classDefinition); 90 | } 91 | final dependencies = classDefinition.dependencies; 92 | dependencies.forEach((dependency) { 93 | List warns; 94 | if (dependency.typeDef.name == 'List') { 95 | // only generate dependency class if the array is not empty 96 | if (jsonRawData[dependency.name].length > 0) { 97 | // when list has ambiguous values, take the first one, otherwise merge all objects 98 | // into a single one 99 | dynamic toAnalyze; 100 | if (!dependency.typeDef.isAmbiguous) { 101 | WithWarning mergeWithWarning = mergeObjectList( 102 | jsonRawData[dependency.name], 103 | '$path/${dependency.name}', 104 | ); 105 | toAnalyze = mergeWithWarning.result; 106 | warnings.addAll(mergeWithWarning.warnings); 107 | } else { 108 | toAnalyze = jsonRawData[dependency.name][0]; 109 | } 110 | final node = navigateNode(astNode, dependency.name); 111 | warns = _generateClassDefinition(dependency.className, toAnalyze, 112 | '$path/${dependency.name}', node, nullSafe); 113 | } 114 | } else { 115 | final node = navigateNode(astNode, dependency.name); 116 | warns = _generateClassDefinition( 117 | dependency.className, 118 | jsonRawData[dependency.name], 119 | '$path/${dependency.name}', 120 | node, 121 | nullSafe); 122 | } 123 | if (warns != null) { 124 | warnings.addAll(warns); 125 | } 126 | }); 127 | } 128 | return warnings; 129 | } 130 | 131 | /// generateUnsafeDart will generate all classes and append one after another 132 | /// in a single string. The [rawJson] param is assumed to be a properly 133 | /// formatted JSON string. The dart code is not validated so invalid dart code 134 | /// might be returned 135 | DartCode generateUnsafeDart(String rawJson, bool nullSafe) { 136 | final jsonRawData = decodeJSON(rawJson); 137 | final astNode = parse(rawJson, Settings()); 138 | List warnings = _generateClassDefinition( 139 | _rootClassName, jsonRawData, "", astNode, nullSafe); 140 | // after generating all classes, replace the omited similar classes. 141 | allClasses.forEach((c) { 142 | final fieldsKeys = c.fields.keys; 143 | fieldsKeys.forEach((f) { 144 | final typeForField = c.fields[f]; 145 | if (sameClassMapping.containsKey(typeForField.name)) { 146 | c.fields[f].name = sameClassMapping[typeForField.name]; 147 | } 148 | }); 149 | }); 150 | return DartCode(allClasses.map((c) => c.toString()).join('\n'), warnings); 151 | } 152 | 153 | /// generateDartClasses will generate all classes and append one after another 154 | /// in a single string. The [rawJson] param is assumed to be a properly 155 | /// formatted JSON string. If the generated dart is invalid it will throw an error. 156 | DartCode generateDartClasses( 157 | String rawJson, { 158 | bool nullSafe = false, 159 | }) { 160 | final unsafeDartCode = generateUnsafeDart(rawJson, nullSafe); 161 | final formatter = DartFormatter(); 162 | return DartCode( 163 | formatter.format(unsafeDartCode.code), unsafeDartCode.warnings); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /bin/json_to_dart/syntax.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_ast/json_ast.dart' show Node; 2 | import '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 Warning(emptyListWarn, path); 17 | } 18 | 19 | Warning newAmbiguousListWarn(String path) { 20 | return Warning(ambiguousListWarn, path); 21 | } 22 | 23 | Warning newAmbiguousType(String path) { 24 | return 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 nullSafe; 38 | bool isAmbiguous = false; 39 | bool _isPrimitive = false; 40 | 41 | factory TypeDefinition.fromDynamic(dynamic obj, Node astNode, bool nullSafe) { 42 | bool isAmbiguous = false; 43 | final type = getTypeName(obj); 44 | if (type == 'List') { 45 | List list = obj; 46 | String elemType; 47 | if (list.length > 0) { 48 | elemType = getTypeName(list[0]); 49 | for (dynamic listVal in list) { 50 | if (elemType != getTypeName(listVal)) { 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 TypeDefinition(type, nullSafe, 60 | astNode: astNode, subtype: elemType, isAmbiguous: isAmbiguous); 61 | } 62 | return TypeDefinition(type, nullSafe, 63 | astNode: astNode, isAmbiguous: isAmbiguous); 64 | } 65 | 66 | TypeDefinition(this.name, bool nullSafe, 67 | {this.subtype, this.isAmbiguous, Node astNode}) { 68 | this.nullSafe = nullSafe; 69 | if (subtype == null) { 70 | _isPrimitive = isPrimitiveType(this.name); 71 | if (this.name == 'int' && isASTLiteralDouble(astNode)) { 72 | this.name = 'double'; 73 | } 74 | } else { 75 | _isPrimitive = isPrimitiveType('$name<$subtype>'); 76 | } 77 | if (isAmbiguous == null) { 78 | isAmbiguous = false; 79 | } 80 | } 81 | 82 | bool operator ==(other) { 83 | if (other is TypeDefinition) { 84 | TypeDefinition otherTypeDef = other; 85 | return this.name == otherTypeDef.name && 86 | this.subtype == otherTypeDef.subtype && 87 | this.isAmbiguous == otherTypeDef.isAmbiguous && 88 | this._isPrimitive == otherTypeDef._isPrimitive; 89 | } 90 | return false; 91 | } 92 | 93 | bool get isPrimitive => _isPrimitive; 94 | 95 | bool get isPrimitiveList => _isPrimitive && name == 'List'; 96 | 97 | String _buildParseClass(String expression) { 98 | final properType = subtype != null ? subtype : name; 99 | return ' $properType.fromJson($expression)'; 100 | } 101 | 102 | String _buildToJsonClass(String expression, bool conditional) { 103 | if (conditional) { 104 | return '$expression!.toJson()'; 105 | } else { 106 | return '$expression.toJson()'; 107 | } 108 | } 109 | 110 | String jsonParseExpression(String key, bool privateField) { 111 | final jsonKey = "json['$key']"; 112 | final fieldKey = 113 | fixFieldName(key, typeDef: this, privateField: privateField); 114 | if (isPrimitive) { 115 | if (name == "List") { 116 | return "$fieldKey = json['$key'].cast<$subtype>();"; 117 | } 118 | return "$fieldKey = json['$key'];"; 119 | } else if (name == "List" && subtype == "DateTime") { 120 | return "$fieldKey = json['$key'].map((v) => DateTime.tryParse(v));"; 121 | } else if (name == "DateTime") { 122 | return "$fieldKey = DateTime.tryParse(json['$key']);"; 123 | } else if (name == 'List') { 124 | // list of class 125 | if (this.nullSafe) { 126 | return "if (json['$key'] != null) {\n\t\t\t$fieldKey = <$subtype>[];\n\t\t\tjson['$key']!.forEach((v) { $fieldKey!.add( $subtype.fromJson(v)); });\n\t\t}"; 127 | } else { 128 | return "if (json['$key'] != null) {\n\t\t\t$fieldKey = <$subtype>[];\n\t\t\tjson['$key'].forEach((v) { $fieldKey.add( $subtype.fromJson(v)); });\n\t\t}"; 129 | } 130 | } else { 131 | // class 132 | return "$fieldKey = json['$key'] != null ? ${_buildParseClass(jsonKey)} : null;"; 133 | } 134 | } 135 | 136 | String toJsonExpression(String key, bool privateField) { 137 | final fieldKey = 138 | fixFieldName(key, typeDef: this, privateField: privateField); 139 | final thisKey = 'this.$fieldKey'; 140 | if (isPrimitive) { 141 | return "data['$key'] = $thisKey;"; 142 | } else if (name == 'List') { 143 | // class list 144 | if (this.nullSafe) { 145 | return """if ($thisKey != null) { 146 | data['$key'] = $thisKey!.map((v) => ${_buildToJsonClass('v', false)}).toList(); 147 | }"""; 148 | } else { 149 | return """if ($thisKey != null) { 150 | data['$key'] = $thisKey.map((v) => ${_buildToJsonClass('v', false)}).toList(); 151 | }"""; 152 | } 153 | } else { 154 | // class 155 | if (this.nullSafe) { 156 | return """if ($thisKey != null) { 157 | data['$key'] = ${_buildToJsonClass(thisKey, true)}; 158 | }"""; 159 | } else { 160 | return """if ($thisKey != null) { 161 | data['$key'] = ${_buildToJsonClass(thisKey, false)}; 162 | }"""; 163 | } 164 | } 165 | } 166 | } 167 | 168 | class Dependency { 169 | String name; 170 | final TypeDefinition typeDef; 171 | 172 | Dependency(this.name, this.typeDef); 173 | 174 | String get className => camelCase(name); 175 | } 176 | 177 | class ClassDefinition { 178 | final String _name; 179 | final bool _privateFields; 180 | final bool _nullSafe; 181 | final Map fields = Map(); 182 | 183 | String get name => _name; 184 | bool get privateFields => _privateFields; 185 | 186 | List get dependencies { 187 | final dependenciesList = []; 188 | final keys = fields.keys; 189 | keys.forEach((k) { 190 | final f = fields[k]; 191 | if (!f.isPrimitive) { 192 | dependenciesList.add(Dependency(k, f)); 193 | } 194 | }); 195 | return dependenciesList; 196 | } 197 | 198 | ClassDefinition( 199 | this._name, [ 200 | this._privateFields = false, 201 | this._nullSafe = false, 202 | ]); 203 | 204 | bool operator ==(other) { 205 | if (other is ClassDefinition) { 206 | ClassDefinition otherClassDef = other; 207 | return this.isSubsetOf(otherClassDef) && otherClassDef.isSubsetOf(this); 208 | } 209 | return false; 210 | } 211 | 212 | bool isSubsetOf(ClassDefinition other) { 213 | final List keys = this.fields.keys.toList(); 214 | final int len = keys.length; 215 | for (int i = 0; i < len; i++) { 216 | TypeDefinition otherTypeDef = other.fields[keys[i]]; 217 | if (otherTypeDef != null) { 218 | TypeDefinition typeDef = this.fields[keys[i]]; 219 | if (typeDef != otherTypeDef) { 220 | return false; 221 | } 222 | } else { 223 | return false; 224 | } 225 | } 226 | return true; 227 | } 228 | 229 | hasField(TypeDefinition otherField) { 230 | return fields.keys 231 | .firstWhere((k) => fields[k] == otherField, orElse: () => null) != 232 | null; 233 | } 234 | 235 | addField(String name, TypeDefinition typeDef) { 236 | fields[name] = typeDef; 237 | } 238 | 239 | void _addTypeDef(TypeDefinition typeDef, StringBuffer sb) { 240 | if (this._nullSafe) { 241 | if (typeDef.subtype != null) { 242 | sb.write('${typeDef.name}'); 243 | sb.write('<${typeDef.subtype}>?'); 244 | } else { 245 | sb.write('${typeDef.name}?'); 246 | } 247 | } else { 248 | sb.write('${typeDef.name}'); 249 | if (typeDef.subtype != null) { 250 | sb.write('<${typeDef.subtype}>'); 251 | } 252 | } 253 | } 254 | 255 | String get _fieldList { 256 | return fields.keys.map((key) { 257 | final f = fields[key]; 258 | final fieldName = 259 | fixFieldName(key, typeDef: f, privateField: privateFields); 260 | final sb = StringBuffer(); 261 | sb.write('\t'); 262 | _addTypeDef(f, sb); 263 | sb.write(' $fieldName;'); 264 | return sb.toString(); 265 | }).join('\n'); 266 | } 267 | 268 | String get _gettersSetters { 269 | return fields.keys.map((key) { 270 | final f = fields[key]; 271 | final publicFieldName = 272 | fixFieldName(key, typeDef: f, privateField: false); 273 | final privateFieldName = 274 | fixFieldName(key, typeDef: f, privateField: true); 275 | final sb = StringBuffer(); 276 | sb.write('\t'); 277 | _addTypeDef(f, sb); 278 | sb.write( 279 | ' get $publicFieldName => $privateFieldName;\n\tset $publicFieldName('); 280 | _addTypeDef(f, sb); 281 | sb.write(' $publicFieldName) => $privateFieldName = $publicFieldName;'); 282 | return sb.toString(); 283 | }).join('\n'); 284 | } 285 | 286 | String get _defaultPrivateConstructor { 287 | final sb = StringBuffer(); 288 | sb.write('\t$name({'); 289 | var i = 0; 290 | var len = fields.keys.length - 1; 291 | fields.keys.forEach((key) { 292 | final f = fields[key]; 293 | final publicFieldName = 294 | fixFieldName(key, typeDef: f, privateField: false); 295 | _addTypeDef(f, sb); 296 | sb.write(' $publicFieldName'); 297 | if (i != len) { 298 | sb.write(', '); 299 | } 300 | i++; 301 | }); 302 | sb.write('}) {\n'); 303 | fields.keys.forEach((key) { 304 | final f = fields[key]; 305 | final publicFieldName = 306 | fixFieldName(key, typeDef: f, privateField: false); 307 | final privateFieldName = 308 | fixFieldName(key, typeDef: f, privateField: true); 309 | sb.write('this.$privateFieldName = $publicFieldName;\n'); 310 | }); 311 | sb.write('}'); 312 | return sb.toString(); 313 | } 314 | 315 | String get _defaultConstructor { 316 | final sb = StringBuffer(); 317 | sb.write('\t$name({'); 318 | var i = 0; 319 | var len = fields.keys.length - 1; 320 | fields.keys.forEach((key) { 321 | final f = fields[key]; 322 | final fieldName = 323 | fixFieldName(key, typeDef: f, privateField: privateFields); 324 | sb.write('this.$fieldName'); 325 | if (i != len) { 326 | sb.write(', '); 327 | } 328 | i++; 329 | }); 330 | sb.write('});'); 331 | return sb.toString(); 332 | } 333 | 334 | String get _jsonParseFunc { 335 | final sb = StringBuffer(); 336 | sb.write('\t$name'); 337 | sb.write('.fromJson(Map json) {\n'); 338 | fields.keys.forEach((k) { 339 | sb.write('\t\t${fields[k].jsonParseExpression(k, privateFields)}\n'); 340 | }); 341 | sb.write('\t}'); 342 | return sb.toString(); 343 | } 344 | 345 | String get _jsonGenFunc { 346 | final sb = StringBuffer(); 347 | sb.write( 348 | '\tMap toJson() {\n\t\tfinal Map data = Map();\n'); 349 | fields.keys.forEach((k) { 350 | sb.write('\t\t${fields[k].toJsonExpression(k, privateFields)}\n'); 351 | }); 352 | sb.write('\t\treturn data;\n'); 353 | sb.write('\t}'); 354 | return sb.toString(); 355 | } 356 | 357 | String toString() { 358 | if (privateFields) { 359 | return 'class $name {\n$_fieldList\n\n$_defaultPrivateConstructor\n\n$_gettersSetters\n\n$_jsonParseFunc\n\n$_jsonGenFunc\n}\n'; 360 | } else { 361 | return 'class $name {\n$_fieldList\n\n$_defaultConstructor\n\n$_jsonParseFunc\n\n$_jsonGenFunc\n}\n'; 362 | } 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /bin/json_to_dart/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 | -------------------------------------------------------------------------------- /build/json2dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacifio/json2dart/eb09c60dbf51df1474292148f9852cc7e0d249e7/build/json2dart -------------------------------------------------------------------------------- /build/json2dart.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacifio/json2dart/eb09c60dbf51df1474292148f9852cc7e0d249e7/build/json2dart.exe -------------------------------------------------------------------------------- /media/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pacifio/json2dart/eb09c60dbf51df1474292148f9852cc7e0d249e7/media/example.gif -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "22.0.0" 11 | analyzer: 12 | dependency: transitive 13 | description: 14 | name: analyzer 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.7.2" 18 | args: 19 | dependency: "direct main" 20 | description: 21 | name: args 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.2.0" 25 | async: 26 | dependency: transitive 27 | description: 28 | name: async 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "2.8.1" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.3.1" 39 | cli_util: 40 | dependency: transitive 41 | description: 42 | name: cli_util 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.3.3" 46 | collection: 47 | dependency: transitive 48 | description: 49 | name: collection 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.15.0" 53 | convert: 54 | dependency: transitive 55 | description: 56 | name: convert 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "3.0.1" 60 | crypto: 61 | dependency: transitive 62 | description: 63 | name: crypto 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "3.0.1" 67 | dart_style: 68 | dependency: "direct main" 69 | description: 70 | name: dart_style 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "1.3.14" 74 | file: 75 | dependency: transitive 76 | description: 77 | name: file 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "6.1.2" 81 | glob: 82 | dependency: transitive 83 | description: 84 | name: glob 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "2.0.1" 88 | grapheme_splitter: 89 | dependency: transitive 90 | description: 91 | name: grapheme_splitter 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "1.0.0" 95 | http: 96 | dependency: "direct main" 97 | description: 98 | name: http 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "0.13.3" 102 | http_parser: 103 | dependency: transitive 104 | description: 105 | name: http_parser 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "4.0.0" 109 | json_ast: 110 | dependency: "direct main" 111 | description: 112 | name: json_ast 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "1.0.5" 116 | meta: 117 | dependency: transitive 118 | description: 119 | name: meta 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.7.0" 123 | package_config: 124 | dependency: transitive 125 | description: 126 | name: package_config 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "2.0.0" 130 | path: 131 | dependency: transitive 132 | description: 133 | name: path 134 | url: "https://pub.dartlang.org" 135 | source: hosted 136 | version: "1.8.0" 137 | pedantic: 138 | dependency: transitive 139 | description: 140 | name: pedantic 141 | url: "https://pub.dartlang.org" 142 | source: hosted 143 | version: "1.11.1" 144 | pub_semver: 145 | dependency: transitive 146 | description: 147 | name: pub_semver 148 | url: "https://pub.dartlang.org" 149 | source: hosted 150 | version: "2.0.0" 151 | source_span: 152 | dependency: transitive 153 | description: 154 | name: source_span 155 | url: "https://pub.dartlang.org" 156 | source: hosted 157 | version: "1.8.1" 158 | string_scanner: 159 | dependency: transitive 160 | description: 161 | name: string_scanner 162 | url: "https://pub.dartlang.org" 163 | source: hosted 164 | version: "1.1.0" 165 | term_glyph: 166 | dependency: transitive 167 | description: 168 | name: term_glyph 169 | url: "https://pub.dartlang.org" 170 | source: hosted 171 | version: "1.2.0" 172 | typed_data: 173 | dependency: transitive 174 | description: 175 | name: typed_data 176 | url: "https://pub.dartlang.org" 177 | source: hosted 178 | version: "1.3.0" 179 | watcher: 180 | dependency: transitive 181 | description: 182 | name: watcher 183 | url: "https://pub.dartlang.org" 184 | source: hosted 185 | version: "1.0.0" 186 | yaml: 187 | dependency: transitive 188 | description: 189 | name: yaml 190 | url: "https://pub.dartlang.org" 191 | source: hosted 192 | version: "3.1.0" 193 | sdks: 194 | dart: ">=2.12.0 <3.0.0" 195 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: json2dartc 2 | description: A CLI tool to help generate dart classes from json returned from API 3 | version: 1.0.0 4 | homepage: https://www.github.com/pacifio/json2dart 5 | 6 | environment: 7 | sdk: '>=2.10.0 <3.0.0' 8 | 9 | executables: 10 | json2dartc: 11 | 12 | dependencies: 13 | json_ast: ^1.0.5 14 | args: ^2.2.0 15 | http: ^0.13.3 16 | dart_style: ^1.3.13 --------------------------------------------------------------------------------