.NET 6 System.Text.Json.JsonSerializer反序列化UTF-8转义字符串

fhg3lkii  于 2022-12-14  发布在  .NET
关注(0)|答案(2)|浏览(431)

我有一些JSON文件(facebook备份),是UTF-8编码,但特殊字符被转义。转义字符也是UTF-8编码,但在十六进制格式。例如:

{
  "sender_name": "Tam\u00c3\u00a1s"
}

我想使用System.Text.Json.JsonSerializer进行反序列化,问题是它将转义的十六进制解释为UTF-16字符,因此它将被反序列化为“Tamás”,而不是它应该的“Tamás”。
要重现的代码:

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

class Msg
{
    [JsonPropertyName("sender_name")]
    public string SenderName { get; set; }
}   

public class Program
{
    public static void Main()
    {
        var data = @"{
            ""sender_name"": ""Tam\u00c3\u00a1s""
        }";
        var msg = JsonSerializer.Deserialize<Msg>(data);
        Console.WriteLine(msg.SenderName);
    }
}

我可以改变序列化器把它解释成UTF-8吗?

afdcj2ne

afdcj2ne1#

这里的问题是JSON的发送者在字符串常量中为á的数字转义码指定了错误的值\u00c3\u00a1\uXXXX转义序列的含义由JSON Proposal和JSON标准指定。它的定义是XXXX是字符的“4 HEXDIG”UTF-16 Unicode码点值[1]。对于á,它是\u00E1。相反,JSON文件的提供商(显然是Facebook的“备份数据功能”)使用UTF-8十六进制值作为\uXXXX转义序列,而不是标准要求的UTF-16。
没有内置的方法来告诉System.Text.Json(或Json .NET)\uXXXX转义序列使用了非标准值,但是Utf8JsonReader通过ValueSpanValueSequence属性提供了对底层原始字节流的访问,因此可以创建一个自定义JsonConverter<string>,该JsonConverter<string>执行必要的解码和取消转义。
首先,创建以下转换器:

public class StringConverterForUtf8EscapedCharValues : JsonConverter<string>
{
    public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.String)
            throw new JsonException();

        if (!reader.ValueIsEscaped)
            return reader.GetString();

        ReadOnlySpan<byte> span = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan;

        // Normally a JSON string will be a utf8 byte sequence with embedded utf18 escape codes.  
        // These improperly encoded JSON strings are utf8 byte sequences with embedded utf8 escape codes.

        var encoding = Encoding.UTF8;
        var decoder = encoding.GetDecoder();
        var sb = new StringBuilder();
        var maxCharCount = Encoding.UTF8.GetMaxCharCount(4);
        
        for (int i = 0; i < span.Length; i++)
        {
            if (span[i] != '\\')
            {
                Span<char> chars = stackalloc char[maxCharCount];
                var n = decoder.GetChars(span.Slice(i, 1), chars, false);
                sb.Append(chars.Slice(0, n));
            }
            else if (i < span.Length - 1 && span[i+1] == '"')
            {
                sb.Append('"');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == '\\')
            {
                sb.Append('\\');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == '/')
            {
                sb.Append('/');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'b')
            {
                sb.Append('\u0008');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'b')
            {
                sb.Append('\u0008');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'f')
            {
                sb.Append('\u0008');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'f')
            {
                sb.Append('\u000C');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'f')
            {
                sb.Append('\u000C');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'n')
            {
                sb.Append('\n');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 'r')
            {
                sb.Append('\r');
                i++;
            }
            else if (i < span.Length - 1 && span[i+1] == 't')
            {
                sb.Append('\t');
                i++;
            }
            else if (i < span.Length - 5 && span[i+1] == 'u')
            {
                Span<char> hexchars = stackalloc char[4] { (char)span[i+2], (char)span[i+3], (char)span[i+4], (char)span[i+5] };
                if (!byte.TryParse(hexchars, NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo, out var b))
                {
                    throw new JsonException();
                }
                Span<char> chars = stackalloc char[maxCharCount];
                Span<byte> bytes = stackalloc byte[1] { b };
                var n = decoder.GetChars(bytes, chars, false);
                sb.Append(chars.Slice(0, n));
                i += 5;
            }
            else
            {
                throw new JsonException();
            }
        }
        var s = sb.ToString();
        return s;
    }

    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}

现在你可以

var options = new JsonSerializerOptions
{
    Converters = { new StringConverterForUtf8EscapedCharValues() },
};
var msg = JsonSerializer.Deserialize<Msg>(data, options);
Assert.That(msg?.SenderName?.StartsWith("Tamás") == true); // Succeeds
Console.WriteLine(msg?.SenderName); // Prints Tamás

注:

  • 由于JSON文件通常是UTF-8编码的字符流,因此解码格式良好的JSON文件中的单个字符串文字可能需要解码UTF-8和UTF-16值的混合。
  • 如果基础字节流不是使用UTF-8编码的,则转换器可能无法工作。
  • 未实现使用(不正确的)UTF-8值写入转义字符。
  • 在将JSON字符串文本解码为c#string之前,应该修复不正确的转义值,因为一旦解码和取消转义完成,转义序列的存在与否就会丢失。
  • 我还没有测试性能,使用Encoding.UTF8.GetDecoder()返回的解码器进行块解码可能性能更好,而不是像这个原型那样逐个字节地解码。

演示小提琴here
[1]不在基本多语言平面中的字符应使用两个连续的转义序列,例如\uD834\uDD1E

lqfhib0f

lqfhib0f2#

试试这个代码

var msg = JsonSerializer.Deserialize<Msg>(data);

    msg.SenderName= DecodeFromUtf16ToUtf8(msg.SenderName); // Tamás

public  string DecodeFromUtf16ToUtf8(string utf16String)
{
    // copy the string as UTF-8 bytes.
    byte[] utf8Bytes = new byte[utf16String.Length];
    for (int i = 0; i < utf16String.Length; ++i)
            utf8Bytes[i] = (byte)utf16String[i];
    
    return Encoding.UTF8.GetString(utf8Bytes, 0, utf8Bytes.Length);
}

也可以添加JSON构造函数

var msg = System.Text.Json.JsonSerializer.Deserialize<Msg>(data);

public class Msg
{
    [JsonPropertyName("sender_name")]
    public string SenderName { get; set; }
    
    public Msg(string SenderName)
    {
        this.SenderName= DecodeFromUtf16ToUtf8(SenderName);
    }
}

相关问题