Починить JSON для System.Text.Json
Столкнулся с проблемой при переходе с NewtonJson на System.Text.Json а именно с капризным восприятием JSON при десериализации:
- Все наименования полей должны быть закавычены
- Все кавычки полей и значений должны быть только двойными.
Всвязи с чем назрел вопрос, как это исправить поддержав обратную совместимость? Я накидал код на коленке, но думаю есть более элегантное решение. Если кто подскажет, то буду безмерно благодарен.
пример, надо исправить следующие JSON {id:444} или {'id':444}, превратив их в {"id":444} ну, естественно если есть входящие поля, то исправить и их
Например надо починить следующий JSON
{
'a' : '"2222 ' ,
a2: true,
"a3":null,
b : {
"c": "' text \"{} []\"",
'c2': '"othertext \'"'
},
"d":[ 'r', "tt"]
}
Код чинилки:
public static class JsonHelper
{
public static string RepairJson(string json)
{
var result = new StringBuilder(json.Length + json.Length / 10);
var charValue = "1234567890truefalse.null";
char quote = ' ';
bool openQuote = false;
bool readValue = false;
bool readUnQuoteName = false;
var lastChar = ' ';
foreach (var c in json)
{
bool WriteQuote = false;
if (readValue && !openQuote && char.IsWhiteSpace(c))
{
continue;
}
if (lastChar != '\\' && (c == '\'' || c == '"'))
{
if (!openQuote)
quote = c;
if (!openQuote || c == quote)
{
WriteQuote = true;
openQuote = !openQuote;
}
}
if (!openQuote && c == ':')
{
if (readUnQuoteName)
{
result.Append("\"");
readUnQuoteName = false;
}
readValue = true;
}
else
{
if (!openQuote && readValue && !charValue.Contains(c))
{
readValue = false;
}
if (!openQuote && !readValue && !readUnQuoteName)
{
var cu = char.ToUpper(c);
if (c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_')
{
result.Append("\"");
readUnQuoteName = true;
}
}
if (!openQuote && readUnQuoteName && char.IsWhiteSpace(c))
{
result.Append("\"");
readUnQuoteName = false;
}
if (openQuote && lastChar != '\\' && quote == '\'' && c == '"')
result.Append('\\');
}
if (WriteQuote)
result.Append('"');
else
result.Append(c);
if (lastChar == '\\' && c == '\\')
lastChar = ' ';
else
lastChar = c;
}
return result.ToString();
}
}
Ответы (1 шт):
Поковырялся, задача и так и сяк сводится к разбору JSON, так как исходя из его структуры, требуется определенная реакция на символ, а это делается примерно так. При этом в невалидном JSON нужно корректно разэскейпить строки.
Вот мой наколеночный вариант.
enum ExpectedToken
{
Name,
Value,
Comma
}
static void Main(string[] args)
{
string text = File.ReadAllText("sample.json");
var ms = new MemoryStream();
var options = new JsonWriterOptions { Indented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
using (var writer = new Utf8JsonWriter(ms, options))
{
ExpectedToken state = ExpectedToken.Value;
var wayBack = new Stack<JsonTokenType>();
for (int i = 0; i < text.Length; i++)
{
i = SkipWhiteSpace(text, i);
if (i >= json.Length) break;
if (state == ExpectedToken.Value)
{
switch (text[i])
{
case '{':
writer.WriteStartObject();
wayBack.Push(JsonTokenType.StartObject);
state = ExpectedToken.Name;
break;
case '[':
writer.WriteStartArray();
wayBack.Push(JsonTokenType.StartArray);
break;
default:
i = ReadValue(text, i, out string value, out bool isString);
if (isString)
writer.WriteStringValue(value);
else
writer.WriteRawValue(value);
state = ExpectedToken.Comma;
break;
}
continue;
}
if (state == ExpectedToken.Name)
{
i = ReadPropertyName(text, i, out string name);
writer.WritePropertyName(name);
state = ExpectedToken.Value;
continue;
}
if (state == ExpectedToken.Comma)
{
switch (text[i])
{
case '}':
writer.WriteEndObject();
wayBack.Pop();
break;
case ']':
writer.WriteEndArray();
wayBack.Pop();
break;
case ',':
if (wayBack.Peek() == JsonTokenType.StartObject)
state = ExpectedToken.Name;
if (wayBack.Peek() == JsonTokenType.StartArray)
state = ExpectedToken.Value;
break;
}
}
}
}
Console.WriteLine(Encoding.UTF8.GetString(ms.ToArray()));
}
private static int SkipWhiteSpace(string text, int index)
{
while (index < text.Length && char.IsWhiteSpace(text[index]))
index++;
return index;
}
private static int ReadPropertyName(string text, int index, out string name)
{
char quote = text[index] == '\'' || text[index] == '"' ? text[index] : '\0';
int i = text.IndexOf(':', index + 1);
name = text[index..i].TrimEnd();
if (quote != '\0')
name = name.Trim(quote);
return i;
}
private static int ReadValue(string text, int index, out string value, out bool isString)
{
char quote = text[index] == '\'' || text[index] == '"' ? text[index] : '\0';
int i = index;
if (quote != '\0')
{
i++;
while (i < text.Length && text[i] != quote)
{
if (text[i] == '\\')
i++;
i++;
}
isString = true;
value = Regex.Unescape(text[(index + 1)..i]);
}
else
{
while (i < text.Length && !char.IsWhiteSpace(text[i]) && text[i] != ',' && text[i] != '}' && text[i] != ']')
i++;
isString = false;
value = text[index..i];
i--;
}
return i;
}
Очень грубо, ничего не оптимизировал, но работает
Вывод в консоль
{
"a": "\"2222 ",
"a2": true,
"a3": null,
"b": {
"c": "' text \"{} []\"",
"c2": "\"othertext '\""
},
"d": [
"r",
"tt"
]
}
От вашего отличается вывод в поле c2, у вас лишний \. На гениальность не претендую. Чтобы убрать отступы пробелами, просто уберите Indented = true.
В качестве оптимизации, можно на спаны переписать, а выход писать не в строку, а в поток. Чисто теоретически можно и на входе от строк избавиться, тогда получится вообще быстро, но я сильно этот вариант не обдумывал.