Library / SDK
nmdias/FeedKit avatar
nmdias/FeedKit

FeedKit: a Swift library for reading and writing feeds

FeedKit is a Swift library for Reading and Generating RSS, Atom, and JSON feeds.

1,354 stars201 forksSwiftMIT

At a glance

What is it?
FeedKit parses and generates RSS, RDF, Atom and JSON feeds in Swift, with typed models for each format, a long list of supported namespaces from Dublin Core to iTunes and Geo RSS, and a universal Feed enum that detects the format before parsing. It is a parsing and serialisation library, with no scheduler, cache or refresh policy.
Who is it for?
FeedKit fits a Swift application that has to read a feed whose format it does not control, or that has to write one, and that wants typed models with namespace coverage instead of hand-mapped XML elements. It does not fit a background sync service, because there is no scheduler, no cache and no refresh policy in the library.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 26 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A library that reads feeds and also writes them

FeedKit is a Swift library for reading and generating RSS, RDF, Atom and JSON feeds, and the two halves are equal citizens rather than a parser with a token serialiser bolted on.

The format coverage is the standard set. Atom, RSS, RDF covering RSS 1.0 and 0.90, and JSON Feed, with the specifications linked in the feature list. The README also lists examples, documentation and unit tests as shipped features, and the repository carries a CI workflow and Swift Package Index badges for supported Swift versions and platforms.

What is not in the library is worth saying first, because it shapes who uses it. There is no scheduler, no caching layer, no ETag or conditional request handling, and no subscription model. FeedKit is the part that turns bytes into models and models back into bytes. Whether you fetch a feed every fifteen minutes or once a day, and what you do with the result, is your code.

That makes it a good fit for two quite different jobs. One is an application that consumes a feed the user pastes in, where the format genuinely varies and a wrong guess means an empty screen. The other is a tool that has to produce a valid feed, because a generator that only reads is of no use to a publishing pipeline.

The licence is MIT, and the last push to the repository was on 2026-09-27, with version 10.9.4 published the same day.

Dedicated types, or one enum that sniffs the format

The reader API has two shapes, and the choice is between knowing your format and not knowing it.

The dedicated types are RSSFeed, RDFFeed, AtomFeed and JSONFeed, and when you know the type of feed to be read you use the dedicated type. The call is a single throwing async initialiser:

swift
try await RSSFeed(urlString: "https://developer.apple.com/news/rss/news.rss")

The universal type is the Feed enum, and it handles RSS, RDF, Atom and JSON, determining the type of feed before reading, parsing and decoding occurs:

swift
// Read any type of feed
let feed = try await Feed(urlString: "https://surprise.example/feed")

// Use a switch to get the resulting feed model
switch feed {
case let .atom(feed): // An AtomFeed instance
case let .rss(feed): // An RSSFeed instance
case let .rdf(feed): // An RDFFeed instance
case let .json(feed): // A JSONFeed instance
}

There is a cost to that convenience, and it is the price of the Swift type system. The result of the universal initialiser is an enum, so every use site has to handle all four cases, and the compiler makes you handle them rather than letting you forget one. In code that runs per feed, per item, that switch is a small tax paid on every access, and it is the kind of thing that shows up in a code review as an unexplained nested switch.

For an app that reads a user-supplied feed URL, that is exactly the right trade. For a service that knows every feed it consumes is RSS, the dedicated type is strictly better, and the only reason to use the enum is that you are not sure.

FeedType answers the format question without paying for the parse

There is a third option that sits between the two, and it is the one worth knowing about.

The FeedType enum identifies the type of a feed without the overhead of parsing and decoding. The README's point is that the universal Feed enum already determines the type automatically, so you generally do not need to do it manually, but the manual path is useful when you want to know the format and nothing else.

swift
let feedType = try FeedType(data: data)

// Detect feed type
switch feedType {
case .rss: // RSS feed detected
case .rdf: // RDF feed detected
case .atom: // Atom feed detected
case .json: // JSON feed detected
}

// Or feed content type
if feedType.isXML {
  // XML feed detected
} else if feedType.isJSON {
  // JSON feed detected
}

The isXML and isJSON properties are the part that earns its place in real applications. Sniffing the container format is a cheap operation, and it lets an application decide between two entirely different decoding paths before committing to one. A client that supports both XML feeds and JSON feeds can check the container first and then parse the matching model, rather than trying the XML decoder and falling back.

The two properties also cover the case where the content is neither, since an XML detector and a JSON detector can both be false, and the README does not describe a third case. That is a small gap in the documentation rather than in the library, but it is the kind of thing to check before relying on the booleans to drive a parse path.

Six initialisers cover every way a feed arrives

All the feed types conform to FeedInitializable and share multiple common initialisers, which the README says provide a flexible way to read feeds from the most common sources.

The set is worth reading as a list of the ways a feed actually reaches your code. From a URL string there is init(urlString: String) async throws. From a URL, handling both local file URLs and remote URLs, there is init(url: URL) async throws. And then the specific ones: init(fileURL url: URL) throws for a local file, init(remoteURL url: URL) async throws for a remote one, init(string: String) throws for XML or JSON text, and init(data: Data) throws for raw bytes.

The split between throwing and async throwing is the useful signal here. The synchronous initialisers are for content you already have, such as a local file, a string or raw data, and the asynchronous ones cover the URL-based sources. A caller reading a feed from disk does not need to suspend, and a caller that has already fetched the bytes does not need to await anything.

The URL initialiser is the one that handles both file and remote URLs, which is convenient and slightly dangerous in a different way: a feed URL that happens to resolve to a local path is read from disk. For an application that takes a URL from a user, that is a behaviour to check rather than assume.

The Data and String initialisers are the ones that make the library testable, and the fact that the README lists unit tests as a shipped feature suggests they are used that way.

Generation is a model, a formatter flag and a string

The writer side is deliberately symmetrical with the reader, and the example makes it clear.

To generate an XML string for any given XML feed, you create an instance of an RSSFeed or AtomFeed and populate it with the necessary data:

swift
let feed = RSSFeed(
  channel: .init(
    title: "Breaking News",
    link: "http://www.breakingnews.example/",
    description: "Get the latest updates as they happen.",
    // ...
    items: [
      .init(
        title: "Breaking News: All Hearts are Joyful",
        link: "http://breakingnews.example/2025/01/09/joyful-hearts",
        description: "A heartwarming story of unity and celebration."
        // ...
      ),
    ]
  )
)

And then you call toXMLString(formatted:) to generate the XML string. In the same way, for JSON, you create a JSONFeed, populate it, and call toJSONString(formatted:). The formatted flag is the only knob, and it exists because a feed that is going to be committed to a repository should be readable and a feed that is going to be sent over the wire should be compact.

The output is a plain string, not a URL or a request, which is what makes the generator usable. A publishing script can write it to a file, a server can serve it, and a test can assert on its contents. The README shows the generated XML in full, which is the kind of example that saves a reader from wondering whether the output is valid.

One detail the README notes, for the RDF model, is worth knowing before you write code against it: in RSS 1.0 the items are siblings of the channel, not children of it. The preview shows feed.channel?.title for RDF where the RSS preview shows channel.title, and the difference is a direct consequence of the format rather than a design choice.

The namespace list is where the actual coverage is

The feature list has a section for namespaces, and it is longer than the format list above it. There are thirteen of them: Atom, Dublin Core, Syndication, Content, Media RSS, Geo RSS, GML, iTunes, YouTube, Podcast, Feed Paging and Archiving, the Comment API, and Podlove Simple Chapters.

This is where a feed library earns its place, because the format specification is only the skeleton. A real podcast feed is an RSS document with iTunes elements; a YouTube channel feed carries the YouTube namespace; a map feed carries Geo RSS coordinates; an archived feed carries the paging and archiving elements from RFC 5005. Parsing the base format and discarding the rest is what produces an app that shows titles but no durations, no artwork and no episode numbers.

The model preview shows the shape of the result. The RSS channel exposes title, link, description, language, copyright, managingEditor, webMaster, pubDate, lastBuildDate, categories, generator, docs, cloud, rating, ttl, image, textInput, skipHours and skipDays, and then the namespace extensions as separate properties, so channel.dublinCore, channel.syndication, channel.iTunes and channel.atom. The item exposes title, link, description, author, categories, comments, enclosure, guid, pubDate and source, with item.dublinCore, item.content, item.iTunes and item.media alongside.

That structure is the design decision to accept or to reject. The models are wide, which is the price of the namespace coverage, and a Swift developer working with optional properties and nested namespaces will spend time on optional chaining. The alternative, which is what a lot of teams actually do, is to parse the feed with a generic XML parser and hand-map the handful of elements you care about, which is less code up front and a maintenance burden the moment the feed publisher changes.

Ten major versions of evidence, and a benchmarks directory

The version numbers tell their own story. The recent releases are 10.9.2, 10.9.3 and 10.9.4, published on 25 September and 27 September 2026, and the project is at version 10.x. A library that has crossed ten major versions with a patch release every few days at the end of that line is a library that has been in use for a long time.

The repository layout supports the same conclusion. There is a Tests directory and a Benchmarks directory, which is not something a young library has. There is an ARCHITECTURE_REVIEW.md and a PERFORMANCE.md at the root, which read as working documents rather than marketing. There is a format.sh alongside a .swiftformat configuration, so the formatting is enforced rather than suggested. And there is an .agents directory and a .spi.yml, the latter being the configuration for publishing to Swift Package Index.

The two package manifests are the detail worth knowing. Package.swift is the current one, and [email protected] is a separate manifest for older toolchains, which is how Swift Package Manager supports a version-specific manifest. If your project is not on a recent Swift, the manifest resolution will pick the right one, but it does mean the package supports a range of toolchains, and a very old Swift will resolve to a manifest that may not expose the newest features.

The Example directory contains an Xcode project, so the fastest way to see the library work is to open it, and the documentation is published through Swift Package Index rather than on a separate site.

Editorial conclusion

FeedKit fits a Swift application that has to read a feed whose format it does not control, or that has to write one, and that wants typed models with namespace coverage instead of hand-mapped XML elements. It does not fit a background sync service, because there is no scheduler, no cache and no refresh policy in the library. Before you adopt it, check the Swift version you are on against the two package manifests in the repository, and read the model preview for your format, because the RSS model in particular is wide enough that you will be selecting fields rather than scanning them.

Frequently asked questions

Which feed formats does FeedKit support?

Atom, RSS, RDF covering RSS 1.0 and 0.90, and JSON Feed, each with a dedicated type, plus a universal Feed enum that determines the type before reading, parsing and decoding occurs.

How do I detect a feed type without parsing it?

Use the FeedType enum, for example try FeedType(data: data), and switch on the result, or check the isXML and isJSON properties. The README notes this is useful when you want to identify the type of feed without the overhead of parsing and decoding.

How do I generate a feed with FeedKit?

Create an instance of RSSFeed, AtomFeed or JSONFeed, populate it, and call toXMLString(formatted:) or toJSONString(formatted:) to produce the output string. The README shows the generated XML for a sample RSS feed.

Which namespaces does FeedKit support?

Atom, Dublin Core, Syndication, Content, Media RSS, Geo RSS, GML, iTunes, YouTube, Podcast, Feed Paging and Archiving, the Comment API, and Podlove Simple Chapters, each exposed as separate properties on the feed and item models.

Does FeedKit schedule or cache feed updates?

No. The library is for reading and generating feeds: the initialisers cover reading from a URL, a file, a string or raw Data, and the generator produces strings. Refresh timing and caching are left to the application.

Official sources

  1. Issues
  2. License: MIT
  3. nmdias/FeedKit on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nmdias-feedkit.svg)](https://hysenlabs.com/projects/nmdias-feedkit)

Was this useful?