# syn: parsing Rust source code inside procedural macros

> syn turns a stream of Rust tokens into a syntax tree so derive and attribute macros can inspect, validate and rewrite user code. It is a library for macro authors, not a general Rust parser for scripts.

**dtolnay/syn** — Parser for Rust source code

- Repository: https://github.com/dtolnay/syn
- Stars: 3,428 · Forks: 378
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/dtolnay-syn

## What problem syn solves, and who is meant to use it

A procedural macro receives the user's code as a flat stream of tokens. Tokens carry no structure: the compiler hands you an opening brace and a field name and a colon, and nothing tells you where one item ends and the next begins. syn exists to turn that stream into a typed syntax tree. The README describes it as a parsing library for parsing a stream of Rust tokens into a syntax tree of Rust source code.

The audience is narrow and stated plainly. The README says the library is currently geared toward use in Rust procedural macros, while containing some APIs that may be useful more generally. If you are writing a derive macro that needs to know the names and types of a struct's fields, syn is the layer that answers the question. If you are writing a tool that reads Rust files for a different purpose, you are outside the intended use, and the feature set reflects that.

The syntax tree is rooted at syn::File, which represents a full source file. For macro work there are narrower entry points: syn::Item, syn::Expr and syn::Type. Derive macros get their own type, syn::DeriveInput, described in the README as any of the three legal input items to a derive macro.

## How parsing works: parser functions, ParseStream and spans

Parsing in syn is built around parser functions with the signature fn(ParseStream) -> Result<T>. That signature is the whole design in one line. A parser function takes a stream and returns either a value of your chosen type or an error, and it consumes tokens from the stream as it goes.

Every syntax tree node defined by syn is individually parsable, according to the README, and may be used as a building block for custom syntaxes. That composability is the practical difference between syn and a hand-written token matcher. You can parse a syn::Type, then parse a comma, then parse another syn::Type, and the error from the second call points at the right place because the stream remembers where it is.

The README also notes that you may dream up your own brand new syntax without involving any of syn's syntax tree types. The lazy-static example directory demonstrates this: it reimplements the lazy_static crate as a function-like procedural macro whose input is parsed with syn's parsing API rather than mapped onto an existing Rust construct.

Spans are the second half of the mechanism. Every token parsed by syn is associated with a Span that tracks line and column information back to the source of that token. The README's stated purpose for this is error reporting: spans let a procedural macro display detailed error messages pointing to the right places in the user's code. The heapsize example shows the payoff. When a field type does not implement the derived trait, the compiler's message underlines the offending field type rather than the derive attribute.

## Installing syn and writing a first derive macro

syn is published on crates.io and documented on docs.rs, both linked from the README. Add it as a dependency of a crate that is declared as a procedural macro. The README's canonical setup pairs syn with quote, and quote is also what enables syn's printing feature.

```toml
# Cargo.toml
[package]
...

[lib]
proc-macro = true

[dependencies]
syn = "3"
quote = "1"
```

The [lib] section with proc-macro = true is what makes the crate a macro crate rather than an ordinary library. syn = "3" matches the current release line, and the Cargo.toml in the repository lists version 3.0.6.

The macro itself is an ordinary Rust function tagged with a proc_macro_derive attribute. The README's example parses the incoming tokens with parse_macro_input! into a DeriveInput, then hands tokens back to the compiler.

```rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(MyMacro)]
pub fn my_macro(input: TokenStream) -> TokenStream {
    // Parse the input tokens into a syntax tree
    let input = parse_macro_input!(input as DeriveInput);

    // Build the output, possibly using quasi-quotation
    let expanded = quote! {
        // ...
    };

    // Hand the output tokens back to the compiler
    TokenStream::from(expanded)
}
```

What you should see is a crate that compiles and a derive attribute that can be applied to a struct or enum. The README points to examples/heapsize for a complete working implementation, which derives a HeapSize trait computing an estimate of heap memory owned by a value. That example is the better starting point than a blank macro, because it shows the span plumbing end to end. The README also recommends the procedural macro workshop for learning the different types of procedural macros.

## Feature flags and the compile-time bill

The Cargo.toml shows a deliberately fragmented feature set. The default features are derive, parsing, printing, clone-impls and proc-macro. Everything else is opt-in: full, visit, visit-mut, fold, extra-traits, and test.

The README states the intent directly: functionality is aggressively feature gated so your procedural macros enable only what they need, and do not pay in compile time for all the rest. This is a real constraint on how you write a macro crate, not a cosmetic option. If your macro parses expressions, you need the full feature, because the README's list of narrow entry points (Item, Expr, Type) sits alongside a syntax tree that represents most stable Rust source code and some unstable syntax, and the broader coverage is not in the default set.

The printing feature is tied to a dependency. In Cargo.toml, printing = ["dep:quote"], so enabling printing pulls in quote as an optional dependency. The proc-macro feature likewise forwards to proc-macro2/proc-macro and quote?/proc-macro. The practical consequence is that turning off a feature can remove a dependency from your build graph entirely, which is the point of the arrangement.

One more constraint worth checking before you start: the package metadata sets rust-version = "1.71".

## Where syn is the wrong tool, and what to use instead

syn is a parser, not a compiler front end. It does not resolve names, expand other macros, or type-check. A macro that needs to know whether a type implements a trait cannot learn that from syn; the heapsize example gets its answer from the compiler, which emits E0277 after expansion. If your design assumes syn can tell you what a name refers to, the design is wrong.

The second boundary is scope. The README says the syntax tree can represent most stable Rust source code and some unstable syntax. Most is doing work in that sentence. Constructs outside that coverage are not guaranteed to parse, and the README does not enumerate which unstable syntax is included.

The README does not document rollback or recovery for a failed parse. A parser function returns a Result, and the README's examples treat failure as producing an error message rather than a partial tree you can continue from. If you need error-tolerant parsing that recovers and keeps going, that is not what this library is described as offering.

For a different approach, consider rustc's own parser crates, which are what the compiler itself uses. The difference in approach is that those crates are internal to the compiler, tied to a specific nightly toolchain, and unstable by design, whereas syn is a published crate with a stable version line and a documented feature set. syn gives you less of the language and a stable interface to it. If you need the full language and can pin a nightly toolchain and accept breakage, the compiler's parser is the closer match. For macro authors, the tradeoff usually runs the other way.

## Maintenance, licence and what upgrading costs

The repository is not archived, and its most recent push was on 2026-09-22. The 3.x line has moved quickly: 3.0.4 on 2026-08-24, 3.0.5 on 2026-09-04, and 3.0.6 on 2026-09-16. Patch releases at that cadence mean a lockfile will drift if you do not pin, and that a cargo update can pull in parser changes you did not read about. The README links release notes on GitHub, which is where the changes are described.

The licence is dual, MIT OR Apache-2.0, and the repository carries both LICENSE-MIT and LICENSE-APACHE at the top level. Cargo.toml declares license = "MIT OR Apache-2.0". Under a dual licence you choose which terms apply to your use, and the two files contain the actual terms. This is a common arrangement in the Rust ecosystem, but whether either licence fits your distribution model depends on facts about your product that this article cannot assess. Read both files rather than assuming the choice is trivial.

Upgrade cost is dominated by the feature flags and the syntax tree's coverage. A minor version bump inside 3.x is the cheap case. A move across major versions is not, because the entry points your macro depends on (DeriveInput, Item, Expr, Type) and the parser function signature are the surface that changes. The repository ships a tests/ directory and a dev/ directory, and the Cargo.toml defines a test feature that pulls in syn-test-suite/all-features, which is a hint about how the project validates itself, but it tells you nothing about whether your macro's edge cases are covered.

## Conclusion

Adopt syn if you are writing a derive, attribute or function-like procedural macro and need to inspect Rust syntax rather than just pass tokens through. Do not adopt it as a general-purpose Rust parser for a linter, formatter or IDE, because the README frames the library as geared toward procedural macros and the syntax tree only covers most stable Rust plus some unstable syntax. Before committing, verify which feature flags your macro actually needs, since the default set is derive, parsing, printing, clone-impls and proc-macro, and check that the crate's rust-version of 1.71 matches your toolchain policy.

## FAQ

### What is syn used for in Rust?

syn parses a stream of Rust tokens into a syntax tree of Rust source code. The README says it is geared toward use in Rust procedural macros, with entry points like syn::DeriveInput for derive macros and syn::Item, syn::Expr and syn::Type for other macro work.

### How do I add syn to a derive macro crate?

Declare the crate as a procedural macro with proc-macro = true under [lib] in Cargo.toml, then add syn and quote as dependencies. The README's example uses syn = "3" and quote = "1".

### Which syn feature flags do I need?

The default features are derive, parsing, printing, clone-impls and proc-macro. The README says functionality is aggressively feature gated so macros enable only what they need, so features such as full, visit, visit-mut, fold and extra-traits must be turned on explicitly when your macro uses them.

### Does syn report errors at the right place in user code?

It can. Every token parsed by syn is associated with a Span tracking line and column information back to the source, and the README says these spans let a macro display detailed error messages pointing to the right places in the user's code. The heapsize example shows the resulting compiler error underlining the offending field type.

### What licence does syn use?

The package metadata declares license = "MIT OR Apache-2.0", and the repository contains LICENSE-MIT and LICENSE-APACHE at the top level. Under a dual licence you pick which terms apply to your use.

## Sources

- [dtolnay/syn on GitHub](https://github.com/dtolnay/syn)
- [Issues](https://github.com/dtolnay/syn/issues)
- [License: Apache-2.0](https://github.com/dtolnay/syn/blob/master/LICENSE)
- [README](https://github.com/dtolnay/syn/blob/master/README.md)
- [Releases](https://github.com/dtolnay/syn/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dtolnay-syn
