// Copyright © 2010 Xamasoft using Humanizer; using Json2CSharpCodeGenerator.Lib.CodeWriters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Text; namespace Json2CSharpCodeGenerator.Lib; public class JsonClassGenerator : IJsonClassGeneratorConfig { #region IJsonClassGeneratorConfig public OutputTypes OutputType { get; set; } = OutputTypes.MutableClass; public OutputCollectionType CollectionType { get; set; } = OutputCollectionType.MutableList; public MutableClassConfig MutableClasses { get; } = new MutableClassConfig(); public JsonLibrary AttributeLibrary { get; set; } = JsonLibrary.NewtonsoftJson; public JsonPropertyAttributeUsage AttributeUsage { get; set; } = JsonPropertyAttributeUsage.OnlyWhenNecessary; public string Namespace { get; set; } public string SecondaryNamespace { get; set; } public bool InternalVisibility { get; set; } public bool NoHelperClass { get; set; } public string MainClass { get; set; } public bool UsePascalCase { get; set; } public bool UseNestedClasses { get; set; } public bool ApplyObfuscationAttributes { get; set; } public bool SingleFile { get; set; } public ICodeBuilder CodeWriter { get; set; } public bool AlwaysUseNullableValues { get; set; } public bool ExamplesInDocumentation { get; set; } public bool RemoveToJson { get; set; } public bool RemoveFromJson { get; set; } public bool RemoveConstructors { get; set; } #endregion public TextWriter OutputStream { get; set; } //private readonly PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US")); public StringBuilder GenerateClasses(string jsonInput, out string errorMessage) { JObject[] examples = null; bool rootWasArray = false; try { using StringReader sr = new(jsonInput); using JsonTextReader reader = new(sr); JToken json = JToken.ReadFrom(reader); if (json is JArray jArray && (jArray.Count == 0 || jArray.All(el => el is JObject))) { rootWasArray = true; examples = jArray.Cast().ToArray(); } else if (json is JObject jObject) { examples = new[] { jObject }; } } catch (Exception ex) { errorMessage = "Exception: " + ex.Message; return new StringBuilder(); } try { if (CodeWriter == null) CodeWriter = new CSharpCodeWriter(); Types = new List(); _ = this._Names.Add("Root"); JsonType rootType = new(this, examples[0]) { IsRoot = true }; rootType.AssignName("Root"); GenerateClass(examples, rootType); Types = HandleDuplicateClasses(Types); StringBuilder builder = new(); WriteClassesToFile(builder, Types, rootWasArray); errorMessage = string.Empty; return builder; } catch (Exception ex) { errorMessage = ex.ToString(); return new StringBuilder(); } } private void WriteClassesToFile(StringBuilder sw, IEnumerable types, bool rootIsArray = false) { bool inNamespace = false; bool rootNamespace = false; CodeWriter.WriteFileStart(this, sw); CodeWriter.WriteDeserializationComment(this, sw, rootIsArray); foreach (JsonType type in types) { if (this.HasNamespace() && inNamespace && rootNamespace != type.IsRoot && SecondaryNamespace != null) { CodeWriter.WriteNamespaceEnd(this, sw, rootNamespace); inNamespace = false; } if (this.HasNamespace() && !inNamespace) { CodeWriter.WriteNamespaceStart(this, sw, type.IsRoot); inNamespace = true; rootNamespace = type.IsRoot; } CodeWriter.WriteClass(this, sw, type); } if (this.HasNamespace() && inNamespace) { CodeWriter.WriteNamespaceEnd(this, sw, rootNamespace); } CodeWriter.WriteFileEnd(this, sw); } private void GenerateClass(JObject[] examples, JsonType type) { Dictionary jsonFields = new(); Dictionary> fieldExamples = new(); bool first = true; foreach (JObject obj in examples) { foreach (JProperty prop in obj.Properties()) { JsonType fieldType; JsonType currentType = new(this, prop.Value); string propName = prop.Name; if (jsonFields.TryGetValue(propName, out fieldType)) { JsonType commonType = fieldType.GetCommonType(currentType); jsonFields[propName] = commonType; } else { JsonType commonType = currentType; if (first) commonType = commonType.MaybeMakeNullable(this); else commonType = commonType.GetCommonType(JsonType.GetNull(this)); jsonFields.Add(propName, commonType); fieldExamples[propName] = new List(); } List fe = fieldExamples[propName]; JToken val = prop.Value; if (val.Type is JTokenType.Null or JTokenType.Undefined) { if (!fe.Contains(null)) { fe.Insert(0, null); } } else { object v = val.Type is JTokenType.Array or JTokenType.Object ? val : val.Value(); if (!fe.Any(x => v.Equals(x))) { fe.Add(v); } } } first = false; } if (UseNestedClasses) { foreach (KeyValuePair field in jsonFields) { _ = this._Names.Add(field.Key.ToLower()); } } foreach (KeyValuePair field in jsonFields) { JsonType fieldType = field.Value; if (fieldType.Type == JsonTypeEnum.Object) { List subexamples = new(examples.Length); foreach (JObject obj in examples) { JToken value; if (obj.TryGetValue(field.Key, out value)) { if (value.Type == JTokenType.Object) { subexamples.Add((JObject)value); } } } fieldType.AssignOriginalName(field.Key); fieldType.AssignName(CreateUniqueClassName(field.Key)); fieldType.AssignNewAssignedName(ToTitleCase(field.Key)); GenerateClass(subexamples.ToArray(), fieldType); } if (fieldType.InternalType != null && fieldType.InternalType.Type == JsonTypeEnum.Object) { List subexamples = new(examples.Length); foreach (JObject obj in examples) { JToken value; if (obj.TryGetValue(field.Key, out value)) { if (value is JArray jArray) { const int MAX_JSON_ARRAY_ITEMS = 50; // Take like 30 items from the array this will increase the chance of getting all the objects accuralty while not analyzing all the data subexamples.AddRange(jArray.OfType().Take(MAX_JSON_ARRAY_ITEMS)); } else if (value is JObject jObject) //TODO J2C : ONLY LOOP OVER 50 OBJECT AND NOT THE WHOLE THING { foreach (KeyValuePair jsonObjectProperty in jObject) { // if (!(item.Value is JObject)) throw new NotSupportedException("Arrays of non-objects are not supported yet."); if (jsonObjectProperty.Value is JObject innerObject) { subexamples.Add(innerObject); } } } } } field.Value.InternalType.AssignOriginalName(field.Key); field.Value.InternalType.AssignName(CreateUniqueClassNameFromPlural(field.Key)); field.Value.InternalType.AssignNewAssignedName(ToTitleCase(field.Key).Singularize(inputIsKnownToBePlural: false)); GenerateClass(subexamples.ToArray(), field.Value.InternalType); } } type.Fields = jsonFields .Select(x => new FieldInfo( generator: this, jsonMemberName: x.Key, type: x.Value, usePascalCase: UsePascalCase || AttributeUsage == JsonPropertyAttributeUsage.Always, examples: fieldExamples[x.Key]) ) .ToList(); if (!string.IsNullOrEmpty(type.AssignedName)) { Types.Add(type); } } /// Checks if there are any duplicate classes in the input, and merges its corresponding properties (TEST CASE 7) private IList HandleDuplicateClasses(IList types) { // TODO: This is currently O(n*n) because it iterates through List on every loop iteration. This can be optimized. List typesWithNoDuplicates = new(); types = types.OrderBy(p => p.AssignedName).ToList(); foreach (JsonType type in types) { if (!typesWithNoDuplicates.Exists(p => p.OriginalName == type.OriginalName)) { typesWithNoDuplicates.Add(type); } else { JsonType duplicatedType = typesWithNoDuplicates.FirstOrDefault(p => p.OriginalName == type.OriginalName); // Rename all references of this type to the original assigned name foreach (FieldInfo field in type.Fields) { if (!duplicatedType.Fields.ToList().Exists(x => x.JsonMemberName == field.JsonMemberName)) { duplicatedType.Fields.Add(field); } } } } return typesWithNoDuplicates; } public IList Types { get; private set; } private readonly HashSet _Names = new(); private string CreateUniqueClassName(string name) { name = ToTitleCase(name); string finalName = name; int i = 2; while (this._Names.Any(x => x.Equals(finalName, StringComparison.OrdinalIgnoreCase))) { finalName = name + i.ToString(); i++; } _ = this._Names.Add(finalName); return finalName; } private string CreateUniqueClassNameFromPlural(string plural) { plural = ToTitleCase(plural); string singular = plural.Singularize(inputIsKnownToBePlural: false); return CreateUniqueClassName(singular); } internal static string ToTitleCase(string str) { StringBuilder sb = new(str.Length); bool flag = true; for (int i = 0; i < str.Length; i++) { char c = str[i]; string specialCaseFirstCharIsNumber = string.Empty; // Handle the case where the first character is a number if (i == 0 && char.IsDigit(c)) specialCaseFirstCharIsNumber = "_" + c; if (char.IsLetterOrDigit(c)) { if (string.IsNullOrEmpty(specialCaseFirstCharIsNumber)) { _ = sb.Append(flag ? char.ToUpper(c) : c); } else { _ = sb.Append(flag ? specialCaseFirstCharIsNumber.ToUpper() : specialCaseFirstCharIsNumber); } flag = false; } else { flag = true; } } return sb.ToString(); } }