---
title: "Save NLP results as JSON in C#"
url: https://recursiveai.org/onenlp/cookbook/save-nlp-results-as-json/
description: "Save a tokenized document and its annotations as JSON, then restore it as a OneNLP document."
version: "1.0.0"
---

# Save NLP results as JSON in C#

**OneNLP** is a free, open-source natural language processing library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneNlp) · [GitHub](https://github.com/recursive-ai-oss/onenlp-csharp) · [Docs](https://recursiveai.org/onenlp/docs/) · [Download example (.zip)](https://recursiveai.org/onenlp/cookbook/save-nlp-results-as-json/save-nlp-results-as-json.zip)

Save a tokenized document and its annotations as JSON, then restore it as a OneNLP document.

```shell
dotnet add package RecursiveAI.OneNlp
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using System.Text.Json;
using RecursiveAI.OneNlp;

using var nlp = Pipeline.Blank(Language.English);
Doc doc = nlp.Process("Ada ordered tea.");
string json = JsonSerializer.Serialize(doc);
File.WriteAllText("result.json", json);
Doc restored = JsonSerializer.Deserialize<Doc>(
    File.ReadAllText("result.json"))!;
Console.WriteLine(restored.Text);
Console.WriteLine($"Tokens: {restored.Length}");
```

```text
Ada ordered tea.
Tokens: 4
```

The input is “Ada ordered tea.”. A blank pipeline tokenizes it without adding model annotations. The program saves `result.json`, reads it back and prints the restored text and token count.

Invalid document JSON passed through `JsonSerializer.Deserialize<Doc>` throws `JsonException`.

`result.json` contains:

```json
{"text":"Ada ordered tea.","tokens":[{"id":0,"start":0,"end":3},{"id":1,"start":4,"end":11},{"id":2,"start":12,"end":15},{"id":3,"start":15,"end":16}]}
```

Unzip [the example](https://recursiveai.org/onenlp/cookbook/save-nlp-results-as-json/save-nlp-results-as-json.zip), then `dotnet run`.

## Variations

**Use the direct document API.** Run this after creating `doc` in the full program.

```csharp
string direct = doc.ToJson();
Doc copy = Doc.FromJson(direct);
Console.WriteLine(copy.Text);
```

```text
Ada ordered tea.
```

## Related recipes

- [Tokenize text](https://recursiveai.org/onenlp/cookbook/tokenize-text/)
- [Named entity recognition](https://recursiveai.org/onenlp/cookbook/named-entity-recognition/)

## The short answer

Serialize a OneNLP `Doc` with `JsonSerializer.Serialize`, save the resulting string, and read it back with `JsonSerializer.Deserialize<Doc>`. OneNLP supplies the document converter through an attribute on `Doc`; no custom converter registration is needed. The example saves tokenized text as `result.json`, restores it, and prints its text and token count.

A blank pipeline only tokenizes this sentence. It does not add entity labels, part-of-speech tags or dependencies. If your pipeline has produced those annotations, the same document serializer carries the supported fields. The [JSON format documentation](https://recursiveai.org/onenlp/docs/json-format/) specifies which fields are written and how missing values are treated.

## How to serialize a C# object to JSON?

For a `Doc`, use the standard `System.Text.Json` calls shown above. The document’s converter writes the same format as `Doc.ToJson()`. The variation shows the direct document API when you do not need to embed the result in another JSON object.

The output is a document representation, not a serialized trained model. It contains the original text, token boundaries and annotations that were set. Saving it does not save your pipeline’s encoder weights or label heads. To load trained behavior for analyzing new text, [save a pipeline file](https://recursiveai.org/onenlp/docs/model-files/) instead.

The writer omits annotation fields nobody set. This matters when reading stored results: an absent entity field is not the same as an explicitly empty list of entities. The latter means entities were set and none were found. Categories are written as a label-to-score object, and named span groups are written as lists of ranges and labels. Overlapping spans remain overlapping.

## Which NLP annotations and offsets does document JSON preserve?

Process text with the pipeline you need, then serialize the resulting `Doc`, rather than serializing only its text or projecting tokens into an unrelated object. Keeping the document format preserves the connection between the text and its offsets. The example’s `result.json` is shown above so you can inspect exactly what was saved.

JSON offsets count Unicode code points. In-memory `Doc` offsets count UTF-16 code units. Those units differ after a character outside the Basic Multilingual Plane, such as many emoji. The writer and reader convert between them; do not manually reuse a JSON offset as a C# string index after such a character. End positions are exclusive, and entity and sentence ranges must align with token boundaries.

Not every possible in-memory value can be represented in this format. Unpaired surrogates and token boundaries inside surrogate pairs cause `InvalidOperationException` on writing. Unknown fields and `user_data` are skipped on reading because `Doc` does not store them. JSON round-tripping is therefore not a promise to preserve arbitrary application state attached elsewhere.

## How to deserialize JSON in C#?

For this format, deserialize to `Doc`, not to a standalone `Token` or `Span`. Those two types can be written individually but cannot be read on their own. A document provides the text and context their ranges refer to.

An invalid document supplied through `JsonSerializer` throws `JsonException`. The reader checks field types, token indexes, heads, confidence ranges and offsets instead of accepting arbitrary JSON with a matching property name. For NativeAOT applications that wrap a `Doc` inside a DTO, list that DTO in a source-generated `JsonSerializerContext` as usual. The `Doc` converter itself does not rely on reflection.
