Skip to main content

gff3-validator

A one-stop validator for GFF3 annotation files, built by FAIR-bioHeaders with the Sequence Ontology (SO) group.

Status: early release (0.1.0). The rule catalogue is a draft under review with the Sequence Ontology group (FHR-Specification #62). The core syntax, attribute, directive and structure rules that the GFF3 specification settles are implemented, and the optional biology rules run with --genome; Sequence Ontology and header rules are not yet. Install with pip install gff3-validator, or use it in the browser at https://fair-bioheaders.github.io/gff3-validator/. Rule ids, levels and output formats may still change before 1.0.

Goal

One place, one name, three interfaces (web page, CLI, library) that tell you whether a GFF3 file is valid against the GFF3 specification (version 1.26) and the Sequence Ontology, and that explain every problem in plain language with the rule id, the line and a suggested fix. When a file carries a FAIR-bioHeaders (FHGFF3) provenance header, the validator also checks it and its link to the genome. See spec 009 (copied in specs/001-gff3-validator).

Plain GFF3 without a header is a first-class input: core validation does not depend on FAIR-bioHeaders.

Rule catalogue

Every check is a numbered rule in rules/catalogue.yaml, rendered as docs/rules.md. Each rule has an id, level (error, warning, info), layer (core, FHGFF3, biology), a reference into GFF3 1.26 or SO, a bad example and a fix, its implementation status and its SO review state. Open questions for SO are collected in docs/questions-for-SO.md. A suggested fix is guidance only; the validator never rewrites a file.

docs/EVALUATION.md reviews the 2024 FHGFF3 prototype, the LinkML GFF3 model and existing validators (GenomeTools, AGAT, GFF3toolkit, the SO-era Perl validator, NCBI table2asn), with a side-by-side run on small broken files.

Implemented rules

68 of 88 catalogue rules are implemented:

  • GFF-SYN-001 (error): First line is the ##gff-version 3 directive
  • GFF-SYN-002 (error): Only one ##gff-version directive
  • GFF-SYN-003 (error): Feature lines have nine tab-separated columns
  • GFF-SYN-004 (error): Undefined columns are ".", not empty
  • GFF-SYN-005 (error): No unescaped control characters
  • GFF-SYN-006 (warning): File is UTF-8
  • GFF-SYN-007 (info): Possible end-of-line comment
  • GFF-SYN-008 (error): Percent-encoding is well formed
  • GFF-SYN-009 (warning): Characters encoded that need not be
  • GFF-SYN-010 (error): seqid uses the allowed characters
  • GFF-SYN-012 (error): type is defined
  • GFF-SYN-013 (error): start and end are integers
  • GFF-SYN-014 (error): start is at least 1
  • GFF-SYN-015 (error): start is not greater than end
  • GFF-SYN-016 (error): score is a number or "."
  • GFF-SYN-017 (error): strand is +, -, . or ?
  • GFF-SYN-018 (error): phase is 0, 1, 2 or "."
  • GFF-SYN-019 (error): CDS features have a phase
  • GFF-ATT-001 (error): Attributes are tag=value pairs separated by semicolons
  • GFF-ATT-002 (error): Tags and values are not empty
  • GFF-ATT-003 (info): Empty attribute pair or trailing semicolon
  • GFF-ATT-004 (error): Each tag appears once per line
  • GFF-ATT-005 (error): Reserved characters are escaped in tags and values
  • GFF-ATT-006 (error): Single-valued reserved tags have one value
  • GFF-ATT-007 (warning): Unknown reserved (upper-case) tag
  • GFF-ATT-008 (warning): Tag differs from a reserved tag only by case
  • GFF-ATT-009 (info): Quoted attribute value
  • GFF-ATT-010 (error): Target has the form "target_id start end [strand]"
  • GFF-ATT-011 (error): Gap is a list of operations
  • GFF-ATT-012 (warning): Gap is given with Target
  • GFF-ATT-013 (warning): Gap lengths agree with the feature and Target lengths
  • GFF-ATT-014 (error): Dbxref and Ontology_term values are DBTAG:ID
  • GFF-ATT-016 (warning): Is_circular is "true"
  • GFF-DIR-001 (error): ##sequence-region has the form: seqid start end
  • GFF-DIR-002 (error): One ##sequence-region per seqid
  • GFF-DIR-003 (error): ### closes all forward references
  • GFF-DIR-004 (error): Only FASTA after ##FASTA
  • GFF-DIR-005 (warning): FASTA section starts with ##FASTA
  • GFF-DIR-006 (warning): FASTA records are well formed
  • GFF-DIR-007 (warning): ##species is an NCBI Taxonomy URL
  • GFF-DIR-008 (warning): ##genome-build has a source and a build name
  • GFF-DIR-009 (info): Ontology directives are recorded but not fetched
  • GFF-DIR-010 (info): Unknown directive
  • GFF-STR-001 (error): IDs are unique, except for the lines of one discontinuous feature
  • GFF-STR-002 (warning): Lines of one feature share seqid and strand
  • GFF-STR-003 (warning): Lines of one feature have the same Parent
  • GFF-STR-004 (error): Parent refers to an ID in the file
  • GFF-STR-005 (error): Derives_from refers to an ID in the file
  • GFF-STR-006 (error): No Parent cycles
  • GFF-STR-007 (warning): No Derives_from cycles
  • GFF-STR-008 (error): Features are within their ##sequence-region
  • GFF-STR-009 (info): seqid without ##sequence-region
  • GFF-STR-010 (warning): seqids appear in the ##FASTA section
  • GFF-STR-011 (error): Features fit within the embedded sequence
  • BIO-001 (error): seqid is in the genome
  • BIO-002 (error): Features fit within the genome sequence
  • BIO-003 (warning): ##sequence-region agrees with the genome
  • BIO-004 (warning): CDS phases are consistent
  • BIO-005 (warning): CDS segments lie within exons
  • BIO-006 (warning): CDS starts with a start codon
  • BIO-007 (warning): CDS ends with a stop codon
  • BIO-008 (warning): No internal stop codons
  • BIO-009 (info): CDS length is a multiple of three
  • BIO-010 (warning): Strand is consistent within a gene
  • BIO-011 (info): Biology checks skipped
  • HDR-001 (error): FHGFF3 header present when required
  • HDR-002 (info): FHGFF3 header present but not validated
  • HDR-003 (info): Header checks skipped (--no-header)

Planned interfaces

  • CLI gff3-validate FILE [--genome genome.fa [--translation-table N]] [--format text|json|html|sarif], reading plain, gzip/BGZF or stdin (-) input. Exit codes: 0 no errors, 1 errors found, 2 usage error or unreadable input (the run is then incomplete).
  • Library gff3_validator.validate(path) returning findings with rule id, level, line, column, message and fix.
  • Web page running the same engine in the browser through Pyodide, so files never leave the computer (see Use it in your browser).
  • Reports in text, JSON, HTML (one self-contained file) and SARIF 2.1.0 (for code-scanning annotations in CI).
  • Repository profiles (NCBI, Ensembl, Alliance) layered on the core rules, later and only with each repository's review.
  • Distribution on PyPI and Bioconda, a Galaxy wrapper, an FHR-Nextflow module and a container, after the first reviewed release.

Headers: a FAIR-bioHeaders header is validated when present; --require-header makes it mandatory and --no-header skips header checks. Header checks need the optional extra, pip install "gff3-validator[headers]" (FAIR-bioHeaders toolkit 0.4). FHGFF3 support in the toolkit is not released yet, so the skeleton only detects #~ header lines and reports that header validation is not implemented.

Current skeleton

poetry install
poetry run gff3-validate tests/fixtures/valid/canonical_gene.gff3
poetry run gff3-validate --format json annotation.gff3.gz

The reader streams line by line and stops reading features at ##FASTA; FASTA records are read for their ids and lengths only, never kept. Per-line rules keep no state. Findings for problems that can occur on most lines (GFF-SYN-006, GFF-SYN-009, GFF-ATT-003, GFF-ATT-009) are reported once, at the first occurrence, with a count, so they cannot crowd errors out of the --max-findings cap.

Memory

The structure rules (IDs, Parent and Derives_from references, cycles, bounds) need state. Memory is O(number of distinct IDs) plus O(number of seqids):

  • each ID gets an integer index and four packed array entries (first line, seqid/type/strand combination, first Parent, Parent-list hash); features with several Parents or with Derives_from keep the extra edges in sparse maps;
  • features without an ID are not remembered after their line, except for a reference to an ID not yet defined;
  • forward references are kept per referenced ID (first line and count), not per line, and are resolved when the ID appears or at the end of the file; ### only advances a counter, so a reference that crosses it is detected when the ID is finally defined (GFF-DIR-003);
  • per seqid: its first line, furthest start and end, and at most 100 lines beyond its ##sequence-region end (then a count);
  • Parent and Derives_from cycles are found at the end of the file by an iterative depth-first search (one byte of state per ID, no recursion).

On a synthetic file of 3.1 million lines (143,000 genes with two mRNAs, five exons and four CDS segments each, 2.1 million distinct IDs) a run takes about 22 s and 380 MiB peak RSS on Python 3.13, and 40 s and 430 MiB on Python 3.9 (roughly 170 to 200 bytes per ID). The opt-in test regenerates such a file and reports the numbers:

GFF3_VALIDATOR_PERFORMANCE_TEST=1 poetry run pytest -s tests/test_performance.py
# GFF3_VALIDATOR_PERFORMANCE_LINES=3000000 sets the size (default 2000000)

Biology checks (--genome)

gff3-validate --genome genome.fa.gz annotation.gff3
gff3-validate --genome genome.fa --translation-table 11 bacterium.gff3

The biology rules (BIO-001 to BIO-011) run only with --genome; without it the report lists the biology layer as not checked. They compare seqids and lengths with the genome, check CDS phases, CDS within exons, start, stop and internal stop codons, coding length and strand consistency. Codon checks use an NCBI translation table: 1 by default (the table is never inferred from the organism; bacteria, archaea and plastids need --translation-table 11). Partial CDS have no standard marker in GFF3, so the codon rules are warnings. Translation exceptions (selenocysteine, pyrrolysine, readthrough, stops completed by polyadenylation) are exempt when the codon is covered by a recoded_codon child of the CDS (or a subtype), following the SO/NCBI discussion in SO-Ontologies#658 (pending SO confirmation), or by a legacy transl_except attribute.

The genome is a FASTA file, plain or gzip/BGZF (detected from the magic bytes; not stdin). Leading ; lines, such as a FAIR-bioHeaders ;~ header, are skipped. The validator reads the file once to build an in-memory index like samtools faidx (name, length, offset, bases and bytes per line; memory O(number of sequences)) and then reads CDS bases on demand, in 64 kb blocks with a 2 MiB cache, so no chromosome is loaded whole. A gzip genome is first decompressed to an anonymous temporary file in TMPDIR, which needs as much free disk space as the uncompressed genome and is removed at the end. A genome that cannot be indexed reliably (sequence before the first >, duplicate names, lines of different lengths inside a record, non-sequence characters) stops the run with exit code 2.

CDS segments (four integers per line), the exons of each transcript and the recoded codons of each CDS are kept until the next ### or the end of the file, then judged and dropped. With ### after each gene this is a few genes' worth; without ### it is about 40 bytes per CDS line and 20 bytes per exon, plus about 300 bytes per transcript. On a synthetic file of 524,000 lines (23,809 genes on both strands, 47,618 CDS of 1,066 codons) with a 231 MiB genome, a run takes about 9 s and 97 MiB peak RSS on Python 3.13 (14 s and 104 MiB on 3.9); without ### lines, 140 MiB. The opt-in performance test includes this case (GFF3_VALIDATOR_PERFORMANCE_GENOME_LINES sets its size).

Use it in your browser

https://fair-bioheaders.github.io/gff3-validator/ (once GitHub Pages is enabled for this repository) runs the validator inside the page: the gff3-validator wheel built from this repository, in Python compiled to WebAssembly (Pyodide). Drop or pick a .gff3 or .gff3.gz file, optionally add a genome FASTA for the biology checks and a translation table, choose how to treat a FAIR-bioHeaders header, and the page shows the HTML report and offers it, the JSON and the SARIF report for download. The reports are the ones gff3-validate --format html|json|sarif writes for the same file and options; CI checks this on every fixture (web/test/parity.mjs).

Privacy. Your files are read by the page and never uploaded. After the page has loaded, the only requests are for the Python runtime and PyYAML from one pinned Pyodide release on cdn.jsdelivr.net and for the validator wheel from the page's own site. The page's Content-Security-Policy (a meta tag; the validator runs in a Web Worker started from a blob: URL, which inherits it) allows connections only to the site itself and to that Pyodide release, so the page could not send a file elsewhere even by mistake. pyodide.js and pyodide-lock.json are loaded with Subresource Integrity (sha384, in web/pyodide.json); the lock file pins the sha256 of PyYAML, and the page checks the wheel against the sha256 in its manifest.json. The rest of the Pyodide runtime (pyodide.asm.mjs, the WebAssembly binary and the standard library) is loaded by pyodide.js from the same pinned release without SRI, because Pyodide offers no way to pass hashes for them.

Size limits. The annotation is read from disk in 1 MiB slices and is never held in memory; memory grows with the number of feature IDs, as on the command line. Measured in headless Chrome (Pyodide 314.0.7, Python 3.14) on the synthetic files of the performance test:

Input Time WebAssembly memory
1.1 million lines, 90 MB plain (1.05 million IDs) 18 s 155 MiB
3.3 million lines, 28 MB gzip (270 MB uncompressed, 3.15 million IDs) 56 s 387 MiB
524,000 lines (42 MB) with a 231 MiB genome, plain or gzip 18 s 90 to 108 MiB, plus the genome

That is two to three times slower than the CLI. A full vertebrate annotation (about 3 million lines) works. Memory grows by about 120 bytes per ID and WebAssembly memory cannot exceed 4 GiB (browsers may stop a tab sooner), so files several times larger than those measured here (not tested) need the command line. The genome is copied into the page's memory (a gzip genome is also decompressed there), so genomes of a few hundred MB are fine and multi-GB genomes are not; use gff3-validate --genome for those.

Build and try the page locally:

poetry run python scripts/build_web.py      # builds the wheel and web/dist
python -m http.server -d web/dist 8000      # then open http://localhost:8000/

.github/workflows/pages.yml builds web/dist, runs the parity check and deploys to GitHub Pages on every push to main (and on demand). To bump Pyodide, run poetry run python scripts/build_web.py --pin VERSION (it downloads pyodide.js and pyodide-lock.json of that release, computes the SRI hashes, checks that PyYAML is included and rewrites web/pyodide.json), then cd web/test && npm install --save-exact pyodide@VERSION, rebuild and run node web/test/parity.mjs --python "poetry run python".

Development

See CONTRIBUTING.md. The rule catalogue is the source of truth: edit rules/catalogue.yaml, then run python scripts/render_rules.py to regenerate docs/rules.md, the packaged copy and the list above (CI checks for drift with --check).

License

MIT. Copyright (c) 2026 Adam Wright and David Molik (FAIR-bioHeaders contributors).

Metadata

Release files for gff3-validator 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gff3-validator 0.1.0
File Size Uploaded
gff3_validator-0.1.0.tar.gz 66.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gff3-validator 0.1.0
File Interpreter ABI Platform
gff3_validator-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 137.2 kB

Release files / gff3_validator-0.1.0.tar.gz

Download URL gff3_validator-0.1.0.tar.gz
Size 66.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1ac25dda9af64b41ce25cb6042496493c96838879fac90371ba47cfd657aa24f
BLAKE2b-256 checksum
How to use checksums
31be073a330b8eb157dbb5659412d1268d6abcfaf9fa39831ec85cc165e823dc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / gff3_validator-0.1.0-py3-none-any.whl

Download URL gff3_validator-0.1.0-py3-none-any.whl
Size 70.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ea5f457543c11ed5a3ca759eae96daba1574e299cf92b1ddde4db8ffa890dcd6
BLAKE2b-256 checksum
How to use checksums
4c6152a49d7985d78aac0e1f55408a655b689db11950ef2b3eb9c540f0f2a980
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page