System.Text.Json에서 Utf8JsonReader를 사용하는 방법

이 문서에서는 사용자 지정 파서 및 역직렬 변환기를 빌드하는 데 Utf8JsonReader 형식을 사용하는 방법을 보여 줍니다.

Utf8JsonReaderReadOnlySpan<byte> 또는 ReadOnlySequence<byte>에서 읽어온 UTF-8 인코딩 JSON 텍스트를 위한 고성능, 저할당, 전달 전용 판독기입니다. Utf8JsonReader는 사용자 지정 구문 분석기 및 역직렬 변환기를 빌드하는 데 활용할 수 있는 하위 수준 형식입니다. JsonSerializer.Deserialize 메서드는 커버 아래에서 Utf8JsonReader를 사용합니다.

Utf8JsonReader는 Visual Basic 코드에서 직접 사용할 수 없습니다. 자세한 내용은 Visual Basic 지원을 참조하세요.

다음 예제에서는 Utf8JsonReader 클래스 사용 방법을 보여줍니다.

var options = new JsonReaderOptions
{
    AllowTrailingCommas = true,
    CommentHandling = JsonCommentHandling.Skip
};
var reader = new Utf8JsonReader(jsonUtf8Bytes, options);

while (reader.Read())
{
    Console.Write(reader.TokenType);

    switch (reader.TokenType)
    {
        case JsonTokenType.PropertyName:
        case JsonTokenType.String:
            {
                string? text = reader.GetString();
                Console.Write(" ");
                Console.Write(text);
                break;
            }

        case JsonTokenType.Number:
            {
                int intValue = reader.GetInt32();
                Console.Write(" ");
                Console.Write(intValue);
                break;
            }

            // Other token types elided for brevity
    }
    Console.WriteLine();
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

위의 코드는 jsonUtf8 변수가 UTF-8로 인코딩된 유효한 JSON을 포함하는 바이트 배열이라고 가정합니다.

Utf8JsonReader를 사용하여 데이터 필터링

다음 예제는 파일을 동기적으로 읽고 값을 검색하는 방법을 보여줍니다.

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

namespace SystemTextJsonSamples
{
    public class Utf8ReaderFromFile
    {
        private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
        private static ReadOnlySpan<byte> Utf8Bom => new byte[] { 0xEF, 0xBB, 0xBF };

        public static void Run()
        {
            // ReadAllBytes if the file encoding is UTF-8:
            string fileName = "UniversitiesUtf8.json";
            ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);

            // Read past the UTF-8 BOM bytes if a BOM exists.
            if (jsonReadOnlySpan.StartsWith(Utf8Bom))
            {
                jsonReadOnlySpan = jsonReadOnlySpan.Slice(Utf8Bom.Length);
            }

            // Or read as UTF-16 and transcode to UTF-8 to convert to a ReadOnlySpan<byte>
            //string fileName = "Universities.json";
            //string jsonString = File.ReadAllText(fileName);
            //ReadOnlySpan<byte> jsonReadOnlySpan = Encoding.UTF8.GetBytes(jsonString);

            int count = 0;
            int total = 0;

            var reader = new Utf8JsonReader(jsonReadOnlySpan);

            while (reader.Read())
            {
                JsonTokenType tokenType = reader.TokenType;

                switch (tokenType)
                {
                    case JsonTokenType.StartObject:
                        total++;
                        break;
                    case JsonTokenType.PropertyName:
                        if (reader.ValueTextEquals(s_nameUtf8))
                        {
                            // Assume valid JSON, known schema
                            reader.Read();
                            if (reader.GetString()!.EndsWith("University"))
                            {
                                count++;
                            }
                        }
                        break;
                }
            }
            Console.WriteLine($"{count} out of {total} have names that end with 'University'");
        }
    }
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

이 예제의 비동기 버전은 .NET 샘플 JSON 프로젝트를 참조하세요.

위의 코드는

  • JSON에 개체 배열이 포함되고 각 개체에 문자열 형식의 “name” 속성이 포함될 수 있다고 가정합니다.

  • "대학교"로 끝나는 개체 및 "이름" 속성 값의 개수를 계산합니다.

  • 파일이 UTF-16으로 인코딩되고 UTF-8로 코드 변환된다고 가정합니다. UTF-8로 인코딩된 파일은 다음 코드를 사용하여 ReadOnlySpan<byte>로 직접 읽을 수 있습니다.

    ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
    

    파일에 UTF-8 BOM(바이트 순서 표시)이 포함된 경우 제거한 후 바이트를 Utf8JsonReader에 전달해야 합니다. 판독기에는 텍스트가 필요하기 때문입니다. 그렇지 않으면 BOM은 잘못된 JSON으로 간주되고, 판독기에서 예외를 throw합니다.

다음은 위의 코드에서 읽을 수 있는 JSON 샘플입니다. 다음과 같이 "4개 이름 중 2개가 '대학교'로 끝나는 이름"이라는 결과 요약 메시지가 표시됩니다.

[
  {
    "web_pages": [ "https://contoso.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "contoso.edu" ],
    "name": "Contoso Community College"
  },
  {
    "web_pages": [ "http://fabrikam.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "fabrikam.edu" ],
    "name": "Fabrikam Community College"
  },
  {
    "web_pages": [ "http://www.contosouniversity.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "contosouniversity.edu" ],
    "name": "Contoso University"
  },
  {
    "web_pages": [ "http://www.fabrikamuniversity.edu/" ],
    "alpha_two_code": "US",
    "state-province": null,
    "country": "United States",
    "domains": [ "fabrikamuniversity.edu" ],
    "name": "Fabrikam University"
  }
]

Utf8JsonReader를 사용하여 스트림에서 읽기

큰 파일(예: 기가바이트 이상 크기)을 읽을 때 전체 파일을 한 번에 메모리에 로드하지 않아도 되는 경우가 있습니다. 이 시나리오에서는 FileStream을 사용할 수 있습니다.

Utf8JsonReader를 사용하여 스트림에서 읽는 경우 다음 규칙이 적용됩니다.

  • 부분 JSON 페이로드를 포함하는 버퍼는 판독기가 진행할 수 있도록 최소한 그 안에 있는 가장 큰 JSON 토큰만큼 커야 합니다.
  • 버퍼는 최소한 JSON 내에서 가장 큰 공백 시퀀스만큼 커야 합니다.
  • 판독기는 JSON 페이로드의 다음 TokenType을 완전히 읽을 때까지는 읽은 데이터를 추적하지 않습니다. 따라서 버퍼에 바이트가 남아 있으면 판독기에 다시 전달해야 합니다. BytesConsumed를 사용하여 남은 바이트 수를 확인할 수 있습니다.

다음 코드에서는 스트림에서 읽는 방법을 보여 줍니다. 이 예제에서는 MemoryStream을 보여 줍니다. FileStream이 시작 시 UTF-8 BOM을 포함하는 경우를 제외하고는 FileStream에서 비슷한 코드가 작동합니다. 이 경우 남은 바이트를 Utf8JsonReader에 전달하기 전에 버퍼에서 3바이트를 제거해야 합니다. 그렇지 않으면 BOM이 JSON의 유효한 부분으로 간주되지 않으므로 판독기가 예외를 throw합니다.

이 샘플 코드는 4KB 버퍼로 시작하며 크기가 작아 전체 JSON 토큰을 수용할 수 없을 때마다 버퍼 크기를 두 배로 늘립니다. 이는 판독기가 JSON 페이로드에 대한 작업을 진행하는 데 필요합니다. 이 코드 조각에 제공된 JSON 샘플은 10바이트와 같이 매우 작은 초기 버퍼 크기를 설정하는 경우에만 버퍼 크기 증가를 트리거합니다. 초기 버퍼 크기를 10으로 설정하는 경우 Console.WriteLine 문은 버퍼 크기 증가의 원인과 영향을 보여 줍니다. 4KB 초기 버퍼 크기에서 전체 샘플 JSON은 각 Console.WriteLine마다 표시되며 버퍼 크기를 늘릴 필요가 없습니다.

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

namespace SystemTextJsonSamples
{
    public class Utf8ReaderPartialRead
    {
        public static void Run()
        {
            var jsonString = @"{
                ""Date"": ""2019-08-01T00:00:00-07:00"",
                ""Temperature"": 25,
                ""TemperatureRanges"": {
                    ""Cold"": { ""High"": 20, ""Low"": -10 },
                    ""Hot"": { ""High"": 60, ""Low"": 20 }
                },
                ""Summary"": ""Hot"",
            }";

            byte[] bytes = Encoding.UTF8.GetBytes(jsonString);
            var stream = new MemoryStream(bytes);

            var buffer = new byte[4096];

            // Fill the buffer.
            // For this snippet, we're assuming the stream is open and has data.
            // If it might be closed or empty, check if the return value is 0.
            stream.Read(buffer);

            // We set isFinalBlock to false since we expect more data in a subsequent read from the stream.
            var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");

            // Search for "Summary" property name
            while (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals("Summary"))
            {
                if (!reader.Read())
                {
                    // Not enough of the JSON is in the buffer to complete a read.
                    GetMoreBytesFromStream(stream, ref buffer, ref reader);
                }
            }

            // Found the "Summary" property name.
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
            while (!reader.Read())
            {
                // Not enough of the JSON is in the buffer to complete a read.
                GetMoreBytesFromStream(stream, ref buffer, ref reader);
            }
            // Display value of Summary property, that is, "Hot".
            Console.WriteLine($"Got property value: {reader.GetString()}");
        }

        private static void GetMoreBytesFromStream(
            MemoryStream stream, ref byte[] buffer, ref Utf8JsonReader reader)
        {
            int bytesRead;
            if (reader.BytesConsumed < buffer.Length)
            {
                ReadOnlySpan<byte> leftover = buffer.AsSpan((int)reader.BytesConsumed);

                if (leftover.Length == buffer.Length)
                {
                    Array.Resize(ref buffer, buffer.Length * 2);
                    Console.WriteLine($"Increased buffer size to {buffer.Length}");
                }

                leftover.CopyTo(buffer);
                bytesRead = stream.Read(buffer.AsSpan(leftover.Length));
            }
            else
            {
                bytesRead = stream.Read(buffer);
            }
            Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
            reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState);
        }
    }
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://learn.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support

앞의 예제에서는 버퍼 크기를 늘릴 수 있는 크기 제한을 설정하지 않습니다. 토큰 크기가 너무 클 경우 코드는 OutOfMemoryException 예외와 함께 실패할 수 있습니다. 이 문제는 JSON이 크기가 약 1GB 이상인 토큰을 포함하는 경우 발생할 수 있습니다. 1GB 크기가 두 배가 되면 int32 버퍼에 비해 너무 커지기 때문입니다.

ref 구조체 제한 사항

Utf8JsonReader 형식은 ref struct이므로 특정 제한이 있습니다. 예를 들어 ref struct가 아닌 클래스 또는 구조체에 필드로 저장할 수 없습니다.

고성능을 얻으려면 입력 ReadOnlySpan<byte>를 캐시해야 하는데 그 자체가 ref struct이므로 이 형식은 ref struct여야 합니다. 또한 이 Utf8JsonReader 형식은 상태를 유지하기 때문에 변경 가능합니다. 따라서 값이 아닌 참조로 전달해야 합니다. 값으로 전달하면 구조체 복사본이 생성되고 상태 변경 내용이 호출자에게 표시되지 않습니다.

ref 구조체를 사용하는 방법에 대한 자세한 내용은 할당 방지를 참조하세요.

UTF-8 텍스트 읽기

Utf8JsonReader를 사용할 때 가능한 최상의 성능을 얻으려면 UTF-16 문자열이 아닌 UTF-8 텍스트로 이미 인코딩된 JSON 페이로드를 읽으세요. 코드 예제는 Utf8JsonReader를 사용하여 데이터 필터링을 참조하세요.

다중 세그먼트 ReadOnlySequence를 사용하여 읽기

JSON 입력이 ReadOnlySpan<byte>이면 읽기 루프를 진행할 때 판독기의 ValueSpan 속성에서 각 JSON 요소에 액세스할 수 있습니다. 그러나 입력이 ReadOnlySequence<byte>(PipeReader에서 읽은 결과)인 경우에는 일부 JSON 요소가 ReadOnlySequence<byte> 개체의 여러 세그먼트에 걸쳐 있을 수 있습니다. 이러한 요소는 연속 메모리 블록의 ValueSpan에서 액세스할 수 없습니다. 그 대신, 다중 세그먼트 ReadOnlySequence<byte>가 입력으로 사용될 때마다 판독기에서 HasValueSequence 속성을 폴링하여 현재 JSON 요소에 액세스하는 방법을 확인하세요. 다음은 권장 패턴입니다.

while (reader.Read())
{
    switch (reader.TokenType)
    {
        // ...
        ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
            reader.ValueSequence.ToArray() :
            reader.ValueSpan;
        // ...
    }
}

속성 이름 조회에 ValueTextEquals 사용

ValueSpan을 사용하여 바이트 단위 비교를 수행하려면 속성 이름 조회를 위한 SequenceEqual을 호출하지 마세요. 그 대신 JSON에서 이스케이프된 모든 문자를 이스케이프 해제하는 ValueTextEquals를 호출하세요. 다음은 “name”이라는 속성을 검색하는 방법을 보여주는 예제입니다.

private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
while (reader.Read())
{
    switch (reader.TokenType)
    {
        case JsonTokenType.StartObject:
            total++;
            break;
        case JsonTokenType.PropertyName:
            if (reader.ValueTextEquals(s_nameUtf8))
            {
                count++;
            }
            break;
    }
}

Null 값을 null 허용 값 형식으로 읽기

기본 제공 System.Text.Json API는 null을 허용하지 않는 값 형식만 반환합니다. 예를 들어 Utf8JsonReader.GetBooleanbool을 반환합니다. JSON에서 Null을 발견하면 예외를 throw합니다. 다음 예제에서는 Null을 처리하는 두 가지 방법을 보여줍니다. 하나는 null 허용 값 형식을 반환하는 방법이고, 다른 하나는 기본값을 반환하는 방법입니다.

public bool? ReadAsNullableBoolean()
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return null;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}
public bool ReadAsBoolean(bool defaultValue)
{
    _reader.Read();
    if (_reader.TokenType == JsonTokenType.Null)
    {
        return defaultValue;
    }
    if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
    {
        throw new JsonException();
    }
    return _reader.GetBoolean();
}

토큰의 자식 건너뛰기

현재 JSON 토큰의 자식을 건너뛰려고 Utf8JsonReader.Skip() 메서드를 사용합니다. 토큰이 JsonTokenType.PropertyName 형식이면 판독기는 속성 값으로 이동합니다. 다음 코드 조각에서는 판독기를 속성 값으로 이동하는 데 Utf8JsonReader.Skip()을 사용하는 예제를 보여 줍니다.

var weatherForecast = new WeatherForecast
{
    Date = DateTime.Parse("2019-08-01"),
    TemperatureCelsius = 25,
    Summary = "Hot"
};

byte[] jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(weatherForecast);

var reader = new Utf8JsonReader(jsonUtf8Bytes);

int temp;
while (reader.Read())
{
    switch (reader.TokenType)
    {
        case JsonTokenType.PropertyName:
            {
                if (reader.ValueTextEquals("TemperatureCelsius"))
                {
                    reader.Skip();
                    temp = reader.GetInt32();

                    Console.WriteLine($"Temperature is {temp} degrees.");
                }
                continue;
            }
        default:
            continue;
    }
}

디코딩된 JSON 문자열 사용

.NET 7부터는 디코딩된 JSON 문자열을 사용하기 위해 Utf8JsonReader.GetString() 대신 Utf8JsonReader.CopyString 메서드를 사용할 수 있습니다. 항상 새 문자열을 할당하는 GetString()과 달리, CopyString은 현재 소유한 버퍼에 이스케이프되지 않은 문자열을 복사할 수 있습니다. 다음 코드 조각은 CopyString을 사용하여 UTF-16 문자열을 사용하는 예제를 보여줍니다.

var reader = new Utf8JsonReader( /* jsonReadOnlySpan */ );

int valueLength = reader.HasValueSequence
    ? checked((int)reader.ValueSequence.Length)
    : reader.ValueSpan.Length;

char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.AsSpan(0, charsRead);

// Handle the unescaped JSON string.
ParseUnescapedString(source);
ArrayPool<char>.Shared.Return(buffer, clearArray: true);

void ParseUnescapedString(ReadOnlySpan<char> source)
{
    // ...
}

참고 항목