---
title: "Read CoNLL-U files in C#"
url: https://recursiveai.org/onenlp/cookbook/read-conllu-training-data/
description: "CoNLL-U stores sentences with word-level linguistic annotations."
version: "1.0.0"
---

# Read CoNLL-U files 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/read-conllu-training-data/read-conllu-training-data.zip)

CoNLL-U stores sentences with word-level linguistic annotations. Read it to import an annotated corpus rather than analyze raw text.

```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 RecursiveAI.OneNlp.Data;

// tiny.conllu is in the example zip.
var sentences = ConllUReader.Read("tiny.conllu");
foreach (var sentence in sentences)
    Console.WriteLine(sentence.Doc.Text);
```

```text
I don't run.
```

Unzip [the example](https://recursiveai.org/onenlp/cookbook/read-conllu-training-data/read-conllu-training-data.zip), then `dotnet run`.

## Full program

The zip includes this invented `tiny.conllu` input. Columns are separated by tabs, not spaces. The range line `2-3` represents the surface token “don’t”; its two syntactic words follow immediately.

```text
# sent_id = demo-1
# text = I don't run.
1	I	I	PRON	PRP	_	4	nsubj	4:nsubj	_
2-3	don't	_	_	_	_	_	_	_	_
2	do	do	AUX	VBP	_	4	aux	4:aux	_
3	n't	not	PART	RB	_	4	advmod	4:advmod	_
4	run	run	VERB	VB	_	0	root	0:root	SpaceAfter=No
5	.	.	PUNCT	.	_	4	punct	4:punct	_
```

```csharp
using RecursiveAI.OneNlp.Data;

foreach (var sentence in ConllUReader.Read("tiny.conllu"))
{
    Console.WriteLine(sentence.GetComment("sent_id"));
    Console.WriteLine(sentence.Doc.Text);
    Console.WriteLine($"Multiword tokens: {sentence.MultiwordTokens.Count}");
    foreach (var word in sentence.Doc)
        Console.WriteLine($"{word.Text}: {word.Lemma}, {word.Pos}, {word.Dep}, head={word.HeadIndex}");
}
try
{
    using var bad = new StringReader("1\tbroken");
    _ = ConllUReader.Read(bad).ToList();
}
catch (DataFormatException error)
{
    Console.WriteLine($"Bad input at line {error.LineNumber}");
}
```

`dotnet run` prints:

```text
demo-1
I don't run.
Multiword tokens: 1
I: I, PRON, nsubj, head=3
do: do, AUX, aux, head=3
n't: not, PART, advmod, head=3
run: run, VERB, root, head=3
.: ., PUNCT, punct, head=3
Bad input at line 1
```

`head` is a zero-based token index; the root points to itself.

Malformed input throws `DataFormatException` while you enumerate the reader; `LineNumber` is 1-based.

## Variations

**Read training examples.** For training APIs that accept `Example` objects, use the same file with `ReadExamples`.

```csharp
using RecursiveAI.OneNlp.Data;

var examples = ConllUReader.ReadExamples("tiny.conllu").ToList();
Console.WriteLine($"Training examples: {examples.Count}");
```

```text
Training examples: 1
```

## Related recipes

- [Dependency parsing](https://recursiveai.org/onenlp/cookbook/dependency-parsing/)
- [Save NLP results as JSON](https://recursiveai.org/onenlp/cookbook/save-nlp-results-as-json/)

## The short answer

Call `ConllUReader.Read(path)` and enumerate the sentences. Each `ConllUSentence` contains an annotated `Doc` plus the comments, original word forms, multiword-token lines and other fields needed to preserve the corpus. No model package is required: this imports annotations already present in the file.

## How do I read a CoNLL-U file in C#?

Use the reader inside a `foreach`, as in the snippet at the top. It reads lazily, one sentence at a time. Opening a path does not immediately check the entire file: errors can occur when enumeration reaches a malformed sentence. Put error handling around the enumeration, not only around the call that creates the enumerable.

The fixture above has comments, a multiword token and dependency annotations. Its reconstructed text is `I don't run.`. The `# text` comment is retained as metadata, but the document’s surface text is reconstructed from the word forms, multiword-token forms and spacing flags. This matters if a corpus comment and its actual word lines disagree.

### What are the CoNLL-U columns?

The [CoNLL-U specification](https://universaldependencies.org/format.html) defines ten tab-separated columns: ID, FORM, LEMMA, UPOS, XPOS, FEATS, HEAD, DEPREL, DEPS and MISC. An underscore marks an unspecified field. Blank lines separate sentences, and comments precede the word lines of a sentence.

OneNLP maps LEMMA to `Lemma`, UPOS to `Pos`, XPOS to `Tag`, FEATS to `Morph` and DEPREL to `Dep`. The printed `HeadIndex` is a zero-based document token index, not the original one-based HEAD column. HEAD zero, the corpus root convention, becomes a self-headed token. That is why every printed dependency in this tiny fixture points to token index 3, including the root itself.

The enhanced dependency column stays separately in `ConllUSentence.Deps`; do not treat `Token.Dep` as a complete replacement for that column. `Comments` and `GetComment` expose sentence metadata without requiring you to parse comment strings yourself.

### How are multiword tokens represented?

A range such as `2-3` is a surface token spanning syntactic words, not an additional word in the document. Here, `do` and `n't` cover their own portions of `don't`. There is one document token per syntactic word, while `MultiwordTokens` retains the range line.

When the syntactic forms do not concatenate to the surface spelling, the reader assigns nonempty surface slices rather than changing every token into its dictionary form. Use `sentence.Forms` when you need the original FORM column. These reconstruction rules, including the case where there are too few surface characters for the words, are documented in [data formats](https://recursiveai.org/onenlp/docs/data-formats/).

### How do I handle invalid corpus data?

The full program deliberately reads a broken line after the valid fixture and prints its line number. A word line with too few columns is an error; so are out-of-sequence IDs, a word that names itself as its corpus head and a head outside the sentence. Invalid UTF-8 is rejected rather than silently replaced.

The reader has configurable bounds in `ConllUOptions` for line length, sentence lines and comments. Keep streaming rather than collecting an entire large corpus with `ToList` unless your next step needs all examples in memory. `ReadExamples` is the direct bridge to training; it does not train or predict new annotations by itself.
