Start Debugging

System.Text.Json finalmente escreve JSON Lines no .NET 11 Preview 5

.NET 11 Preview 5 adiciona JsonSerializer.SerializeAsyncEnumerable com topLevelValues: true, para que o System.Text.Json possa transmitir JSONL, e não apenas lê-lo.

As notas da biblioteca do .NET 11 Preview 5 fecham uma lacuna que estava aberta desde o .NET 9: o System.Text.Json agora pode serializar JSON Lines, e não apenas desserializá-lo. O lado de leitura chegou duas versões atrás, o lado de escrita estava faltando, e quem exportava JSONL precisava escrever à mão um laço que emitia um documento por linha. O Preview 5 transforma isso em uma única chamada.

O que é JSON Lines de fato

JSON Lines (JSONL) não é um arranjo JSON. É um valor JSON independente por linha, separados por \n, sem colchetes envolventes e sem vírgulas entre os registros:

{"Device":"kitchen","Celsius":21.4}
{"Device":"garage","Celsius":8.1}

Esse formato está em toda parte assim que você começa a transmitir dados: arquivos de log estruturados, exportações grandes de banco de dados e os formatos de entrada/saída em lote usados pelos pipelines de treinamento e avaliação de LLM. A vantagem é que você pode anexar um registro sem reescrever o arquivo, e pode processar o arquivo linha por linha sem nunca ter tudo na memória. Um arranjo JSON de nível superior não oferece nada disso, porque um analisador não tem como saber que o arranjo é válido até ver o ] de fechamento.

O lado de escrita que estava faltando

O System.Text.Json aprendeu a ler vários valores de nível superior no .NET 9, quando o DeserializeAsyncEnumerable ganhou um parâmetro topLevelValues. O Preview 5 adiciona a sobrecarga simétrica no serializador:

record SensorReading(string Device, double Celsius, DateTimeOffset At);

async IAsyncEnumerable<SensorReading> GetReadings()
{
    yield return new("kitchen", 21.4, DateTimeOffset.UtcNow);
    yield return new("garage", 8.1, DateTimeOffset.UtcNow);
}

await using var stream = File.Create("readings.jsonl");
await JsonSerializer.SerializeAsyncEnumerable(
    stream,
    GetReadings(),
    topLevelValues: true);

Com topLevelValues: true você obtém JSONL: cada item escrito como seu próprio documento raiz na sua própria linha. Deixe no valor padrão false e você obtém o comportamento antigo, um único arranjo JSON. As novas sobrecargas aceitam tanto um Stream quanto um PipeWriter, e recebem JsonSerializerOptions ou um JsonTypeInfo<TValue> gerado por código-fonte para uma serialização compatível com AOT, sem reflexão.

Um ciclo completo limpo

Como o lado de leitura já existe, as duas metades agora se encaixam:

await using var input = File.OpenRead("readings.jsonl");

await foreach (var reading in JsonSerializer.DeserializeAsyncEnumerable<SensorReading>(
    input,
    topLevelValues: true))
{
    Console.WriteLine($"{reading.Device}: {reading.Celsius} C");
}

Note que GetReadings é um IAsyncEnumerable<T>, então o serializador puxa os registros à medida que os escreve. Você pode transmitir direto de um cursor de banco de dados ou de uma resposta HTTP para um arquivo JSONL sem materializar antes uma List<T>. Esse é todo o objetivo: memória constante, não importa quantos registros você passe.

Isso ainda é um preview, então as assinaturas exatas podem mudar antes do lançamento de novembro. Mas o formato da API espelha o lado de leitura o suficiente para que seja seguro começar a projetar em torno dele. Se você vinha colando Utf8JsonWriter com quebras de linha manuais para emitir JSONL, esta é a chamada que substitui tudo isso.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar