How to convert a jira xml export to json
- Step 1Export issues as XML from JIRA — From a filtered issue search, choose Export → Export XML (or, per-issue, the XML view at
…/si/jira.issueviews:issue-xml/KEY/KEY.xml). For a whole instance, a JIRA admin can use the backup/export tooling. Save the.xmlfile. - Step 2Open the XML to JSON tool — This is a Pro tool. Drop the
.xmlonto the dropzone. The free tier converts files up to 2 MB for evaluation; signed-in Pro raises the per-file limit to 100 MB. A large project export will exceed the free cap — narrow the JQL or use Pro. - Step 3Keep Parse attributes on — Leave Parse attributes ON — JIRA stores the issue
key,id,customfieldIDs, andlinktypes in attributes. Turning it off would discard exactly the identifiers your migration needs to map. - Step 4Choose type coercion for time fields — Leave Coerce types on so estimate/time-spent seconds parse as numbers for cycle-time maths. Turn it off if any custom field uses zero-padded codes you must preserve (coercion strips leading zeros).
- Step 5Convert and locate the issues — Click Convert to JSON. JIRA's export nests issues under
rss.channel.item(RSS-style). Multiple issues form an array; a single issue is an object. Each item carries summary, type, status, comments, and custom fields. - Step 6Filter and reshape for the target tracker — The tool does no filtering. Use json-path-extractor with a filter like
$.rss.channel.item[?(@.type=='Bug')], then json-key-renamer to map JIRA names to your target's fields, before calling the Linear / GitHub Issues / Shortcut API.
JIRA XML elements after conversion
How common JIRA export constructs land in JSON (Parse attributes on, Coerce types on). Values illustrative.
| JIRA construct | JSON result | Notes |
|---|---|---|
<key id="10001">PROJ-1</key> | key: {"#text":"PROJ-1","@id":10001} | Issue key in #text, internal id in @id |
<summary>Fix login</summary> | summary: "Fix login" | Plain string |
<description><![CDATA[..]]></description> | description: ".." (HTML/wiki string) | CDATA unwrapped |
Multiple <comment author=.. created=..> | comments: { comment: [ {"#text","@author","@created"}, .. ] } | Array when several; object when one |
<customfield id="customfield_10002" key="..story-points"> | customfield: {"@id":..,"@key":..,"customfieldvalues":{..}} | ID/key kept verbatim — NOT mapped to a friendly name |
<timeoriginalestimate seconds="3600">1h</timeoriginalestimate> | {"#text":"1h","@seconds":3600} | Both display text and seconds preserved |
Multiple <label> | labels: { label: ["backend","urgent"] } | Array of strings when several |
Option matrix for JIRA exports
Real controls and their effect on a JIRA XML export. attributePrefix (@) and textNodeName (#text) are fixed defaults.
| Option | JIRA effect | Recommendation |
|---|---|---|
| Parse attributes | On: issue key/@id, customfield/@id, link/@type preserved. Off: all attribute identifiers lost | Keep ON — JIRA's IDs live in attributes |
| Coerce types | Time/estimate seconds and counts become numbers; story points become numbers | Keep on; turn off for zero-padded custom codes |
| Strip namespaces | JIRA XML is largely namespace-free; little effect on most exports | Off — usually unnecessary |
| Indent | Output formatting only (Minified / 2 / 4 spaces) | 2 or 4 for readable analysis files |
Cookbook
Real JIRA export fragments and the JSON the converter returns, plus the follow-up tools that filter, rename, and prep for the target tracker.
An issue with key, status, and a CDATA description
ExampleJIRA puts the issue key text in the element and the internal numeric id in an attribute. Parse attributes surfaces both; the CDATA description is unwrapped to a string.
Input:
<item>
<key id="10001">PROJ-42</key>
<summary>Login button misaligned</summary>
<type id="1">Bug</type>
<status id="3">In Progress</status>
<description><![CDATA[<p>Repro on Safari</p>]]></description>
</item>
Output (Parse attributes ON, Coerce types ON):
{ "item": {
"key": { "#text": "PROJ-42", "@id": 10001 },
"summary": "Login button misaligned",
"type": { "#text": "Bug", "@id": 1 },
"status": { "#text": "In Progress", "@id": 3 },
"description": "<p>Repro on Safari</p>"
} }Comments — array for many, object for one
ExampleAn issue with several comments gives a comment array; an issue with exactly one gives a single object. Normalise before iterating or some issues lose their comment history.
Several comments:
{ "comments": { "comment": [
{ "#text": "Looking into it", "@author": "sam", "@created": ".." },
{ "#text": "Fixed in PR #12", "@author": "lee", "@created": ".." } ] } }
One comment:
{ "comments": { "comment": { "#text": "wontfix", "@author": "sam" } } }
Safe iteration:
const cs = [].concat(item.comments?.comment ?? []);
for (const c of cs) { migrate(c['#text'], c['@author']); }Custom fields keep IDs — map names yourself
ExampleJIRA custom fields carry a numeric customfield_ID and a key attribute; the tool keeps them verbatim and does NOT translate to a friendly name. Build the mapping from your JIRA field config.
Converted:
{ "customfields": { "customfield": [
{ "@id": "customfield_10002", "@key": "com.pyxis..:jira-story-points",
"customfieldname": "Story Points",
"customfieldvalues": { "customfieldvalue": 5 } } ] } }
Note: customfieldname IS present in JIRA's export, so map via it:
const fields = Object.fromEntries(
[].concat(item.customfields?.customfield ?? []).map(f =>
[f.customfieldname, f.customfieldvalues?.customfieldvalue]));
// fields['Story Points'] === 5Filtering to Bugs and renaming for the target tracker
ExampleThe converter does not filter or rename. Chain sibling tools: extract the issue type you want, then remap JIRA field names to your destination's schema before the API import.
After conversion: rss.channel.item = [ ..all issues.. ] 1) /tool/json-path-extractor $.rss.channel.item[?(@.type=='Bug')] -> only Bug issues remain 2) /tool/json-key-renamer summary -> title, description -> body, assignee -> assigneeId -> shape matches the Linear / GitHub Issues create payload 3) import loop calls the target API per issue.
Time tracking for cycle-time analysis
ExampleEstimate and time-spent fields carry the seconds in an attribute. With Parse attributes + Coerce types on, you get clean numbers ready for pandas cycle-time maths.
Input:
<timeoriginalestimate seconds="28800">8h</timeoriginalestimate>
<timespent seconds="21600">6h</timespent>
Output:
{ "timeoriginalestimate": { "#text": "8h", "@seconds": 28800 },
"timespent": { "#text": "6h", "@seconds": 21600 } }
In pandas:
df['est_h'] = df['timeoriginalestimate'].str['@seconds'] / 3600
df['spent_h'] = df['timespent'].str['@seconds'] / 3600Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Custom-field IDs are NOT mapped to friendly names automatically
By designThe converter keeps @id (customfield_10002) and @key verbatim. JIRA's export does include a <customfieldname> element, so you can build the name map yourself (see the cookbook), but the tool will not rename customfield_10002 to Story Points for you.
A single comment / label is an object, not an array
Singleton trapAn issue with one <comment> yields a single object; several yield an array. Migration code that loops item.comments.comment directly will iterate the object's keys on a one-comment issue. Coalesce with [].concat(item.comments?.comment ?? []) before iterating.
Field names are NOT renamed to camelCase
Not transformedJIRA element names come through verbatim (summary, timeoriginalestimate, customfieldvalue) — there is no camelCase remap. Use json-key-renamer after conversion to map them to your target tracker's field names (summary→title, etc.).
No filtering by type or status during conversion
Not supportedThe tool converts the entire export. To keep only Bugs or only Done issues, convert first, then use json-path-extractor with a filter expression such as [?(@.type=='Bug')] or json-key-filter to trim fields.
No progress indicator for large files
ExpectedConversion is a single synchronous parse; there is no streaming or progress bar. A large export will simply take a moment and the result appears when done. The bigger constraint is the file-size limit, not progress UI.
Issue key id is coerced to a number
ReviewWith Coerce types on, the internal @id on <key id="10001"> becomes the number 10001. The human key (PROJ-42) stays a string in #text. If you rely on the internal id as a string elsewhere, turn Coerce types off or cast it back.
Browser-only — very large exports may be slow or hit the limit
Plan limitEverything runs in the browser. Free tier caps files at 2 MB; Pro at 100 MB per file. Exports with tens of thousands of issues (and their comments) can exceed even 100 MB — narrow the JQL to a date range, or convert in Node with fast-xml-parser directly, then load the JSON.
Malformed export XML parses without an error
Silent parsefast-xml-parser tolerates minor malformation without throwing. A truncated export (interrupted download) may yield a structurally wrong object rather than a clear failure. Check the file size against the issue count and validate in a strict XML validator if results look short.
Frequently asked questions
How are JIRA custom fields mapped to JSON?
Each <customfield> keeps its @id (customfield_10002) and @key verbatim, plus a <customfieldname> element and customfieldvalues. The tool does NOT auto-rename IDs to friendly names — but because customfieldname is present, you can build the map yourself: Object.fromEntries(customfields.map(f => [f.customfieldname, f.customfieldvalues.customfieldvalue])).
Why are the issue key and id in different places?
JIRA stores the human key (PROJ-42) as the element text and the internal numeric id in an attribute. With Parse attributes on you get {"#text":"PROJ-42","@id":10001}. Read item.key['#text'] for the key and item.key['@id'] for the internal id.
Should I keep Parse attributes on for JIRA?
Yes — definitely. JIRA hides the most useful identifiers (issue id, customfield id, link type, comment author/date) in attributes. With the option off they are dropped entirely, leaving you unable to map issues during migration.
Does the tool turn a single comment into an array?
No. One <comment> becomes an object; several become an array. Normalise with [].concat(item.comments?.comment ?? []) before iterating so single-comment issues do not silently lose their history.
Can I filter to only Bugs or only Done issues?
Not in the converter — it has no filtering controls. Convert the full export, then use json-path-extractor with $.rss.channel.item[?(@.type=='Bug')] or [?(@.status=='Done')] to isolate exactly the issues you need.
Does it rename JIRA fields to my target tracker's schema?
No — field names come through verbatim. Use json-key-renamer after conversion to map summary→title, description→body, etc. for the Linear / GitHub Issues / Shortcut create payload.
How do I use the time-tracking fields for cycle-time analysis?
Estimate and time-spent fields carry the seconds in a @seconds attribute and a human label in #text. With Parse attributes + Coerce types on you get clean numbers: item.timeoriginalestimate['@seconds']. Divide by 3600 for hours in pandas or a notebook.
My JIRA export is over 100 MB — will the browser handle it?
The Pro per-file limit is 100 MB, and very large all-comments exports can exceed that. Narrow the export with a JQL date range to produce smaller files, or run fast-xml-parser in a Node script for the full export and load the resulting JSON. There is no progress bar — large files just take a moment.
What happens to wiki-markup or HTML in descriptions?
Description and comment bodies wrapped in CDATA are unwrapped to plain strings, preserving the wiki-markup or HTML verbatim. Convert that markup to your target's format (Markdown, ADF, etc.) in your import transform — the tool delivers the raw content, not a converted form.
Is sprint and team data uploaded to JAD Apps?
No. JIRA XML parsing runs entirely in your browser via fast-xml-parser. Sprint contents, estimates, velocity, and internal comments never reach JAD Apps servers — only an anonymous run counter (no content) is recorded for signed-in dashboard stats.
Can I get a flat issue table for a velocity report?
Yes — after conversion, extract the issues with json-path-extractor ($.rss.channel.item), flatten nested fields with json-flattener, then export to a spreadsheet with json-to-csv. The time-tracking @seconds attributes become numeric columns you can divide by 3600 for hours.
Does it convert JIRA wiki markup or ADF to Markdown?
No. The tool delivers description and comment bodies verbatim as strings — whether that is wiki markup, HTML, or stringified ADF. Converting to your target's format (Markdown for GitHub Issues, the destination's rich text for Linear) is a transform you run in your import script after extracting the text.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.