Migrating between RSS, Atom and JSON Feed

Changing formats is mostly a mapping exercise, but the identifier rules decide whether subscribers see a migration or a re-notification of everything.

The element mapping

ConceptRSS 2.0AtomJSON Feed
Feed titlechannel/titlefeed/titletitle
Feed URLchannel/linkfeed/link rel=alternatehome_page_url
Self URLatom:link rel=selffeed/link rel=selffeed_url
Item identifierguidentry/iditems[].id
Item URLlinkentry/link rel=alternateurl
PublishedpubDate (RFC 822)published (RFC 3339)date_published
Updatedatom:updatedupdated (required)date_modified
Summarydescriptionsummarysummary
Full contentcontent:encodedcontent type=htmlcontent_html
Authordc:creator or authorauthor/name (required on entry or feed)authors[].name
Enclosureenclosure url type lengthlink rel=enclosureattachments[].url
Categoriescategorycategory termtags[]
def to_atom(it: Item) -> str:
    # Atom requires an id, a title and an updated timestamp on every entry.
    # A missing updated value is not optional: fall back to published.
    updated = it.updated or it.published
    return "".join([
        "<entry>",
        "<id>" + esc(it.id) + "</id>",
        "<title>" + esc(it.title) + "</title>",
        "<updated>" + rfc3339(updated) + "</updated>",
        "<published>" + rfc3339(it.published) + "</published>",
        "<link rel=\"alternate\" href=\"" + esc(it.url) + "\"/>",
        "<author><name>" + esc(it.author or "Example Team") + "</name></author>",
        "<summary type=\"text\">" + esc(it.summary) + "</summary>",
        "<content type=\"html\">" + esc(it.html) + "</content>",
        "<category term=\"" + esc(it.tags[0]) + "\"/>" if it.tags else "",
        "</entry>",
    ])

Keeping identifiers and consumers intact

  1. Choose one existing identifier field as the source of truth and reuse its exact values in the new format. Published items keep their existing identifiers forever.
  2. Publish the new format at a new URL. Never repurpose an existing feed URL for a different format, because readers cache the content type as well as the address.
  3. Serve both formats from the same application for a defined period, with an announcement on the site and in both feeds.
  4. Keep the old URL alive and redirect it only at the HTTP level with a 301 to the new format's URL, if you must consolidate. A 302 is safer for the first weeks because it is reversible.
  5. Update the discovery links on every page, but leave the old link present for a while so cached pages still work.
  6. Notify the directories and aggregators that matter, and check that the item identifiers survived on their side before removing the old feed.
  7. Remove the old format only after monitoring shows the old URL has been quiet for a full polling cycle of your slowest consumer.
  • Every item has the same identifier in the new feed as in the old one.
  • Every URL in the new feed is absolute and resolves with the same status as before.
  • Dates round-trip through the new format without gaining or losing an hour.
  • The content type of the old URL is unchanged for as long as the old URL is served.
  • Enclosure URLs and lengths are byte-identical for podcast items.
⚠️
The one change that guarantees a support burden is changing item identifiers. Everything else in a format migration can be fixed with a second deploy; re-notified items cannot be un-notified, and users who see a thousand old posts marked unread will unsubscribe.

FAQ

Can one URL serve both RSS and Atom?
Only with content negotiation on Accept, which many readers do not send correctly and validators report confusingly. Two URLs, one link rel=alternate each, is the reliable approach.
How long should I keep the old feed?
At least a full polling cycle of your slowest consumer, which for a podcast directory can be weeks. Since a static file costs almost nothing, keep it indefinitely unless there is a specific reason to retire it.

Choosing a feed format: RSS 2.0, Atom and JSON Feed Atom and how readers consume feeds

Last refreshed 2026-09-18.