Загрузка данных
private string BuildOutputCode()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("using System;");
builder.AppendLine("using System.Collections.Generic;");
builder.AppendLine();
if (!string.IsNullOrWhiteSpace(_rootComment))
{
builder.AppendLine(_rootComment);
builder.AppendLine();
}
foreach (ClassDefinition classDefinition in _classDefinitions)
{
builder.AppendLine("[Serializable]");
builder.AppendLine($"public class {classDefinition.Name}");
builder.AppendLine("{");
foreach (FieldDefinition field in classDefinition.Fields)
{
builder.AppendLine($" public {field.TypeName} {field.Name};");
builder.AppendLine();
}
builder.AppendLine("}");
builder.AppendLine();
}
return builder.ToString();
}
#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace CodeBase.EditorTools
{
public sealed class JsonToCSharpConverterWindow : EditorWindow
{
private const string RootClassName = "Root";
private string _inputJson = string.Empty;
private string _outputCode = string.Empty;
private Vector2 _inputScrollPosition;
private Vector2 _outputScrollPosition;
[MenuItem("Tools/JSON To C# Class Converter")]
public static void Open()
{
JsonToCSharpConverterWindow window = GetWindow<JsonToCSharpConverterWindow>("JSON To C#");
window.minSize = new Vector2(700f, 500f);
window.Show();
}
private void OnGUI()
{
EditorGUILayout.LabelField("JSON", EditorStyles.boldLabel);
_inputScrollPosition = EditorGUILayout.BeginScrollView(
_inputScrollPosition,
GUILayout.MinHeight(180f),
GUILayout.ExpandHeight(true));
_inputJson = EditorGUILayout.TextArea(_inputJson, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(8f);
if (GUILayout.Button("Convert", GUILayout.Height(32f)))
{
ConvertJsonToCSharpClass();
}
EditorGUILayout.Space(8f);
EditorGUILayout.LabelField("C# Class", EditorStyles.boldLabel);
_outputScrollPosition = EditorGUILayout.BeginScrollView(
_outputScrollPosition,
GUILayout.MinHeight(220f),
GUILayout.ExpandHeight(true));
_outputCode = EditorGUILayout.TextArea(_outputCode, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
}
private void ConvertJsonToCSharpClass()
{
if (string.IsNullOrWhiteSpace(_inputJson))
{
_outputCode = "// JSON is empty.";
return;
}
try
{
JToken rootToken = JToken.Parse(_inputJson);
JsonCSharpClassGenerator generator = new JsonCSharpClassGenerator();
_outputCode = generator.Generate(rootToken, RootClassName);
}
catch (Exception exception)
{
_outputCode = $"// Convert failed:\n// {exception.Message}";
}
}
}
internal sealed class JsonCSharpClassGenerator
{
private readonly List<ClassDefinition> _classDefinitions = new List<ClassDefinition>();
private readonly HashSet<string> _usedClassNames = new HashSet<string>();
private string _rootComment = string.Empty;
public string Generate(JToken rootToken, string rootClassName)
{
_classDefinitions.Clear();
_usedClassNames.Clear();
_rootComment = string.Empty;
if (rootToken.Type == JTokenType.Object)
{
BuildClassFromObjects(new List<JObject> { (JObject)rootToken }, rootClassName);
}
else if (rootToken.Type == JTokenType.Array)
{
BuildRootArrayClass((JArray)rootToken, rootClassName);
}
else
{
BuildPrimitiveRootClass(rootToken, rootClassName);
}
return BuildOutputCode();
}
private void BuildRootArrayClass(JArray rootArray, string rootClassName)
{
List<JToken> notNullItems = rootArray
.Where(token => token.Type != JTokenType.Null && token.Type != JTokenType.Undefined)
.ToList();
if (notNullItems.Count == 0)
{
BuildWrapperClass(rootClassName, "List<object>", "Items", "items");
return;
}
bool containsOnlyObjects = notNullItems.All(token => token.Type == JTokenType.Object);
if (containsOnlyObjects)
{
List<JObject> objects = notNullItems
.Cast<JObject>()
.ToList();
BuildClassFromObjects(objects, rootClassName);
_rootComment = $"// Root JSON is an array. Deserialize it as List<{NormalizeClassName(rootClassName)}>.";
return;
}
string itemTypeName = InferType(notNullItems, "Item");
BuildWrapperClass(rootClassName, $"List<{itemTypeName}>", "Items", "items");
}
private void BuildPrimitiveRootClass(JToken rootToken, string rootClassName)
{
string valueTypeName = GetPrimitiveTypeName(rootToken);
BuildWrapperClass(rootClassName, valueTypeName, "Value", "value");
}
private void BuildWrapperClass(
string suggestedClassName,
string valueTypeName,
string fieldName,
string jsonName)
{
string className = GetUniqueClassName(suggestedClassName);
ClassDefinition classDefinition = new ClassDefinition(className);
classDefinition.Fields.Add(new FieldDefinition(valueTypeName, fieldName, jsonName));
_classDefinitions.Add(classDefinition);
}
private string BuildClassFromObjects(List<JObject> objects, string suggestedClassName)
{
string className = GetUniqueClassName(suggestedClassName);
ClassDefinition classDefinition = new ClassDefinition(className);
_classDefinitions.Add(classDefinition);
List<string> jsonPropertyNames = objects
.SelectMany(jsonObject => jsonObject.Properties())
.Select(property => property.Name)
.Distinct()
.ToList();
HashSet<string> usedFieldNames = new HashSet<string>();
foreach (string jsonPropertyName in jsonPropertyNames)
{
List<JToken> propertyValues = objects
.Select(jsonObject => jsonObject[jsonPropertyName])
.Where(value => value != null)
.ToList();
string fieldTypeName = InferType(propertyValues, jsonPropertyName);
string fieldName = GetUniqueFieldName(ToPascalCase(jsonPropertyName), usedFieldNames);
classDefinition.Fields.Add(new FieldDefinition(fieldTypeName, fieldName, jsonPropertyName));
}
return className;
}
private string InferType(List<JToken> tokens, string suggestedName)
{
List<JToken> notNullTokens = tokens
.Where(token => token.Type != JTokenType.Null && token.Type != JTokenType.Undefined)
.ToList();
if (notNullTokens.Count == 0)
{
return "object";
}
bool containsOnlyObjects = notNullTokens.All(token => token.Type == JTokenType.Object);
if (containsOnlyObjects)
{
List<JObject> objects = notNullTokens
.Cast<JObject>()
.ToList();
return BuildClassFromObjects(objects, suggestedName);
}
bool containsOnlyArrays = notNullTokens.All(token => token.Type == JTokenType.Array);
if (containsOnlyArrays)
{
List<JArray> arrays = notNullTokens
.Cast<JArray>()
.ToList();
return InferArrayType(arrays, suggestedName);
}
bool containsComplexToken = notNullTokens.Any(token =>
token.Type == JTokenType.Object ||
token.Type == JTokenType.Array);
if (containsComplexToken)
{
return "object";
}
List<string> primitiveTypes = notNullTokens
.Select(GetPrimitiveTypeName)
.Distinct()
.ToList();
if (primitiveTypes.Count == 1)
{
return primitiveTypes[0];
}
bool containsOnlyNumbers = primitiveTypes.All(typeName =>
typeName == "int" ||
typeName == "long" ||
typeName == "float" ||
typeName == "double" ||
typeName == "decimal");
return containsOnlyNumbers ? "double" : "object";
}
private string InferArrayType(List<JArray> arrays, string suggestedName)
{
List<JToken> items = arrays
.SelectMany(array => array)
.Where(token => token.Type != JTokenType.Null && token.Type != JTokenType.Undefined)
.ToList();
if (items.Count == 0)
{
return "List<object>";
}
string itemClassName = ToSingularPascalCase(suggestedName);
string itemTypeName = InferType(items, itemClassName);
return $"List<{itemTypeName}>";
}
private string GetPrimitiveTypeName(JToken token)
{
switch (token.Type)
{
case JTokenType.Integer:
long integerValue = token.Value<long>();
return integerValue >= int.MinValue && integerValue <= int.MaxValue
? "int"
: "long";
case JTokenType.Float:
return "double";
case JTokenType.Boolean:
return "bool";
case JTokenType.String:
return "string";
case JTokenType.Date:
return "DateTime";
case JTokenType.Guid:
return "Guid";
case JTokenType.TimeSpan:
return "TimeSpan";
default:
return "object";
}
}
private string BuildOutputCode()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("using System;");
builder.AppendLine("using System.Collections.Generic;");
builder.AppendLine("using Newtonsoft.Json;");
builder.AppendLine();
if (!string.IsNullOrWhiteSpace(_rootComment))
{
builder.AppendLine(_rootComment);
builder.AppendLine();
}
foreach (ClassDefinition classDefinition in _classDefinitions)
{
builder.AppendLine("[Serializable]");
builder.AppendLine($"public class {classDefinition.Name}");
builder.AppendLine("{");
foreach (FieldDefinition field in classDefinition.Fields)
{
builder.AppendLine($" [JsonProperty(\"{EscapeJsonPropertyName(field.JsonName)}\")]");
builder.AppendLine($" public {field.TypeName} {field.Name};");
builder.AppendLine();
}
builder.AppendLine("}");
builder.AppendLine();
}
return builder.ToString();
}
private string GetUniqueClassName(string suggestedClassName)
{
string baseClassName = NormalizeClassName(suggestedClassName);
string className = baseClassName;
int index = 2;
while (_usedClassNames.Contains(className))
{
className = $"{baseClassName}{index}";
index++;
}
_usedClassNames.Add(className);
return className;
}
private string GetUniqueFieldName(string suggestedFieldName, HashSet<string> usedFieldNames)
{
string baseFieldName = NormalizeFieldName(suggestedFieldName);
string fieldName = baseFieldName;
int index = 2;
while (usedFieldNames.Contains(fieldName))
{
fieldName = $"{baseFieldName}{index}";
index++;
}
usedFieldNames.Add(fieldName);
return fieldName;
}
private string NormalizeClassName(string value)
{
string className = ToPascalCase(value);
if (IsCSharpKeyword(className))
{
className += "Class";
}
return className;
}
private string NormalizeFieldName(string value)
{
string fieldName = ToPascalCase(value);
if (IsCSharpKeyword(fieldName))
{
fieldName += "Value";
}
return fieldName;
}
private string ToSingularPascalCase(string value)
{
string pascalCase = ToPascalCase(value);
if (pascalCase.EndsWith("ies", StringComparison.OrdinalIgnoreCase) && pascalCase.Length > 3)
{
return pascalCase.Substring(0, pascalCase.Length - 3) + "y";
}
if (pascalCase.EndsWith("s", StringComparison.OrdinalIgnoreCase) &&
!pascalCase.EndsWith("ss", StringComparison.OrdinalIgnoreCase) &&
pascalCase.Length > 1)
{
return pascalCase.Substring(0, pascalCase.Length - 1);
}
return pascalCase + "Item";
}
private string ToPascalCase(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "Value";
}
StringBuilder builder = new StringBuilder();
bool makeUpper = true;
foreach (char character in value)
{
if (!char.IsLetterOrDigit(character))
{
makeUpper = true;
continue;
}
if (builder.Length == 0 && char.IsDigit(character))
{
builder.Append("Value");
}
builder.Append(makeUpper
? char.ToUpperInvariant(character)
: character);
makeUpper = false;
}
return builder.Length == 0
? "Value"
: builder.ToString();
}
private string EscapeJsonPropertyName(string value)
{
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"");
}
private bool IsCSharpKeyword(string value)
{
switch (value)
{
case "abstract":
case "as":
case "base":
case "bool":
case "break":
case "byte":
case "case":
case "catch":
case "char":
case "checked":
case "class":
case "const":
case "continue":
case "decimal":
case "default":
case "delegate":
case "do":
case "double":
case "else":
case "enum":
case "event":
case "explicit":
case "extern":
case "false":
case "finally":
case "fixed":
case "float":
case "for":
case "foreach":
case "goto":
case "if":
case "implicit":
case "in":
case "int":
case "interface":
case "internal":
case "is":
case "lock":
case "long":
case "namespace":
case "new":
case "null":
case "object":
case "operator":
case "out":
case "override":
case "params":
case "private":
case "protected":
case "public":
case "readonly":
case "ref":
case "return":
case "sbyte":
case "sealed":
case "short":
case "sizeof":
case "stackalloc":
case "static":
case "string":
case "struct":
case "switch":
case "this":
case "throw":
case "true":
case "try":
case "typeof":
case "uint":
case "ulong":
case "unchecked":
case "unsafe":
case "ushort":
case "using":
case "virtual":
case "void":
case "volatile":
case "while":
return true;
default:
return false;
}
}
private sealed class ClassDefinition
{
public readonly string Name;
public readonly List<FieldDefinition> Fields = new List<FieldDefinition>();
public ClassDefinition(string name)
{
Name = name;
}
}
private sealed class FieldDefinition
{
public readonly string TypeName;
public readonly string Name;
public readonly string JsonName;
public FieldDefinition(string typeName, string name, string jsonName)
{
TypeName = typeName;
Name = name;
JsonName = jsonName;
}
}
}
}
#endif