# swift-parsing: parser combinators for Swift, with printing as a first class idea

> swift-parsing gives Swift a single composable unit of parsing, so a large parser is built by stacking small ones, and every parser can be inverted into a printer. It is a Point-Free project aimed at parsers that stay fast even when they are assembled from many parts, and the cost is a steep learning curve and a fast moving 0.x API.

**pointfreeco/swift-parsing** — A library for turning nebulous data into well-structured data, with a focus on composition, performance, generality, and ergonomics.

- Repository: https://github.com/pointfreeco/swift-parsing
- Website: https://www.pointfree.co/collections/parsing
- Stars: 1,000 · Forks: 86
- Language: Swift
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/pointfreeco-swift-parsing

## Swift has plenty of parsers but no story that ties them together

The argument the library makes is not that Swift cannot parse, but that Swift parses in fragments. The standard library ships initializers on `Int`, `Double` and `Bool` that attempt to turn strings into values:

```bash
Int("42")          // 42
Int("Hello")       // nil

Double("123.45")   // 123.45
Double("Goodbye")  // nil

Bool("true")       // true
Bool("0")          // nil
```

There are also decoders for structured formats:

```bash
try JSONDecoder().decode(User.self, from: data)
try PropertyListDecoder().decode(Settings.self, from: data)
```

Both of those return nil or throw when the input does not fit, and both are fine for their job. The gap the library points at is the area between them. When you parse a format that is not exactly a schema, such as a comma separated file with a few quirks, you end up stitching together unrelated initializers, methods and ad hoc string manipulation, and the documentation's conclusion is that this leads to less maintainable and less reusable code.

The audience is Swift developers who write parsers by hand. The claim of a single coherent approach means one unit of parsing that composes, rather than a family of unrelated initializers that do not.

## One unit of parsing: small parsers that compose in both directions

The library introduces a single unit of parsing that can be combined into larger parsers. In the worked example from the getting started guide, the input is a multiline string of comma separated user rows and the target is an array of `User` values:

```bash
var input = """
1,Blob,true
2,Blob Jr.,false
3,Blob Sr.,true
"""

struct User {
  var id: Int
  var name: String
  var isAdmin: Bool
}
```

The naive version splits on newlines, splits each row on commas, and then reaches for `Int` and `Bool` initializers inside a `compactMap` closure. The documentation is blunt about why that is a poor shape: it is messy, and it allocates arrays from the split and immediately throws those values away.

The library's answer is to describe how to consume bits from the beginning of the input, which is what a parser value does. The `Parse` type acts as the entry point for a list of parsers to run one after another, consuming from the input. The first row parser parses an integer off the front and then a comma:

```bash
let user = Parse(input: Substring.self) {
  Int.parser()
  ","
}
```

At this point the parser already consumes from the front of the input:

```bash
try user.parse("1,")  // 1
```

That `parse` call returning the value and consuming only the prefix is the whole mental shift. A parser is a value, it is described once, and it is applied to input.

## Stacking parsers produces a tuple, and map turns it into your type

The row parser grows by appending more steps. Taking the name up to the next comma, then the comma, then the admin flag:

```bash
let user = Parse(input: Substring.self) {
  Int.parser()
  ","
  Prefix { $0 != "," }
  ","
  Bool.parser()
}
```

At that stage the parser produces a tuple of `(Int, Substring, Bool)`, and the transformation into the domain type is a `map` on the parser:

```bash
let user = Parse(input: Substring.self) {
  Int.parser()
  ","
  Prefix { $0 != "," }
  ","
  Bool.parser()
}
: .map { User(id: $0, name: String($1), isAdmin: $2) }
```

The tuple is an implementation detail you get for free from stacking, and the `map` is where the domain type appears. The documentation also mentions the alternative of passing the transform closure as the first argument to `Parse` when the parsed data should be more prominent than the parser description.

Two details here are worth pausing on. The `Prefix { $0 != "," }` step matches a run of characters by predicate rather than by a fixed length, which is what lets a name field contain spaces. And the input type is named explicitly, `Substring.self`, because the library is general about what it can consume, so when a parser is written against one input type you have to say which one.

The efficiency claim also lands here. The naive version allocated a split array per row, while this description consumes the input in place, and the library states that parsers composed of many smaller parts should perform as well as highly tuned hand written parsers.

## Generality means a parser on UTF-8 code units plugs into a parser on strings

The third stated property is generality, and the library is specific about what it means. The ability to parse any kind of input into any kind of output, which lets you choose the abstraction level based on how much performance you need or how much correctness you want guaranteed. The example given is that a highly tuned parser written on collections of UTF-8 code units will automatically plug into parsers of strings, arrays, unsafe buffer pointers and more.

That is the design that makes the `input: Substring.self` annotation on `Parse` necessary rather than annoying. The type annotation says which representation the composed parsers run against, and the same description can be reused at a different level. This is why a small parser such as the comma literal in the example does not need to know whether it is being applied to a `Substring`, a `String`, or a byte array.

The practical consequence for a user is that you can start at the convenient level and drop to a lower one only where measurement or correctness demands it, and the rest of your description does not have to change. The documentation does not quantify the performance difference, so the claim to verify is empirical rather than documented.

## Invertibility turns a parser into a printer

The fourth property is invertibility, and it is the one that most distinguishes this library from an ordinary parser combinator package. The ability to invert your parsers so that they are printers, so that well structured data can be transformed back into unstructured data. The stated uses are serialization, sending data over the network, URL routing, and more.

Read that as a consequence of the representation. If a parser is a value that maps input into output, then the inverse mapping is also expressible, and the same description that describes how to read a format can describe how to write it. The document is the description and the transformation is the parser, rather than two separate descriptions that drift apart.

The documentation stops short of showing the inversion API in this section, and the getting started guide is explicitly an abridged version of a longer article in the library's documentation. So inversion is real enough to be a headline feature but you will need the full documentation site to see how it is written. That is worth knowing before you commit, because if you only need to read, the fourth property does you no work at all.

## Building and testing: an Xcode workspace, a SwiftPM package, and a Linux container

The repository is laid out as a Swift package with an Xcode workspace, a playground and a Makefile. The top level includes `Package.swift` and a `Package@swift-6.1.swift` variant, `Parsing.xcworkspace/`, `Parsing.playground/`, `Sources/`, `Tests/`, and a `.spi.yml` for Swift Package Index. The `Makefile` gives you the whole development loop in one place.

Running the test suite on Apple platforms means driving Xcode against the workspace and the `Parsing` scheme for three destinations, an iOS simulator, macOS and a tvOS simulator, with the destination names coming from variables at the top of the Makefile:

```bash
	xcodebuild test \
		-workspace Parsing.xcworkspace \
		-scheme Parsing \
		-destination platform="$(PLATFORM_IOS)"
```

There is also a Linux path that runs the same SwiftPM tests inside a container, and a plain SwiftPM test target using test discovery and parallel execution:

```bash
	swift test \
		--enable-test-discovery \
		--parallel
```

Benchmarks have their own target, run in release configuration against the `swift-parsing-benchmark` executable:

```bash
	swift run -c release swift-parsing-benchmark
```

The default Makefile target is `test`, so a plain `make` on a Mac runs the Apple platform suite. There is also a `format` target that runs `swift format` over the package sources and the tests, and a `generate-variadics` target that regenerates a source file from a generator executable, which tells you the variadic parser builders are partly machine generated.

## Where the library stops, and what to use instead

The first limitation is scope. If your input has a fixed schema, `JSONDecoder` and `PropertyListDecoder` already do the job with less code and no new dependency, and the library's own motivation section points at them as examples of parsers that exist. Reach for this library when the format is irregular enough that a decoder cannot express it, not because parsers are more elegant in general.

The second limitation is the learning curve and the API churn. The project is still on a 0.x version line, with 0.15.0 released on 2026-06-17, 0.15.1 on 2026-08-31 and 0.15.2 on 2026-09-02, so a minor bump can plausibly change call sites, and 0.x carries no compatibility promise. The four property headings also describe aspirations rather than measured guarantees, and the performance claim in particular has no numbers attached in the documentation.

The alternative worth comparing is the hand written approach with regular expressions and string splitting, which is what most Swift codebases do today and what the motivation section is arguing against. The difference in approach is not accuracy, it is structure. A hand written parser for one format is a function that knows one format. A composed parser is a set of small values that can be reused, and the same set of values can be inverted into a printer, which a hand written function cannot offer. If you only ever parse one format once, the hand written version will be shorter. If you parse a family of related formats, or need to emit them as well as read them, the compositional approach starts to pay for its own overhead.

## Conclusion

swift-parsing suits a Swift team that parses text formats by hand, wants the same parser description to drive both reading and writing, and is willing to read the library's documentation and adapt to an API that is still labelled 0.x. It does not suit a project whose input is a fixed schema handled well by JSONDecoder, or a team that wants a stable API surface it can pin for years. Before adopting it, check three things: that the abstraction you need is a parser you can describe rather than a decoder you can annotate, that the version you pin does not break your call sites when the next 0.15 release lands, and that the invertibility claim is worth your time, since printing support is where the library's design differs most from hand written parsing code.

## FAQ

### What is swift-parsing used for?

It is a library for turning unstructured data into structured data in Swift, built around a single composable unit of parsing so that small parsers can be combined into large ones. It also supports inverting a parser into a printer.

### How do I install swift-parsing in a Swift project?

The repository is a Swift Package Manager package with a Package.swift manifest and a Package@swift-6.1.swift variant. The Makefile provides a test target that runs xcodebuild against the Parsing.xcworkspace, and a Linux target that runs swift test inside a container.

### How do I parse a comma separated file with swift-parsing?

Describe the row with Parse, stacking Int.parser(), a comma literal, Prefix { $0 != "," } and Bool.parser(), then map the resulting tuple onto your own type. The guide shows try user.parse("1,") consuming only the prefix of the input.

### Is swift-parsing suitable for a production project?

The project is published under 0.x version numbers, with 0.15.2 released on 2026-09-02, so it carries no compatibility promise. The documented performance and invertibility claims are stated without supporting numbers.

### What licence is swift-parsing under?

The repository carries an MIT licence in its LICENSE file at the top level of the project.

## Sources

- [License: MIT](https://github.com/pointfreeco/swift-parsing/blob/main/LICENSE)
- [pointfreeco/swift-parsing on GitHub](https://github.com/pointfreeco/swift-parsing)
- [Project website](https://www.pointfree.co/collections/parsing)
- [README](https://github.com/pointfreeco/swift-parsing/blob/main/README.md)
- [Releases](https://github.com/pointfreeco/swift-parsing/releases)

---

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