Auto-Creating Jira Issues from Violation JSON
To create a Jira issue from a violation record without spawning a duplicate on the next scan, attach the finding’s fingerprint as a Jira label, run a JQL search for that label before you write, and only POST /rest/api/3/issue when the search returns nothing — otherwise transition or update the existing issue. This page gives the exact Jira REST API v3 create payload, the ADF description body, and the label-based idempotency lookup in Python requests.
This is the Jira binding of the tracker-agnostic router described in remediation routing: violation JSON to issue tracker, which itself sits within the Enterprise WCAG Audit Architecture & Standards Mapping section. The parent guide owns the fingerprint algorithm and the create/update/close decision; this page assumes you already have a stable fingerprint and focuses only on constructing a valid Jira Cloud request and making the write idempotent through JQL.
When This Applies
Reach for this once the routing decision is sound but the Jira write is wrong. The symptoms are Jira-specific:
- The create call returns
400with"description: Operation value must be an Adf object"— you sent a plain string where Jira Cloud v3 requires Atlassian Document Format. - Every nightly run adds a new issue with an identical summary because nothing queried Jira for the fingerprint first.
priorityor a custom field is rejected with"Field 'priority' cannot be set"because the field is not on the project’s create screen.- Labels silently drop characters — Jira labels cannot contain spaces, so a fingerprint with a space is truncated and the lookup never matches.
If your finding record itself is malformed or unvalidated, fix that upstream first; this page assumes a schema-valid record and a fingerprint already computed by the parent router.
Minimal Reproducible Example
The naive create posts a plain-string description and never checks for an existing issue. Both faults are visible immediately: the request fails on modern Jira Cloud, and even if it succeeded it would duplicate.
import requests
# BROKEN on Jira Cloud v3: description must be ADF, and no dedup lookup runs.
payload = {
"fields": {
"project": {"key": "A11Y"},
"issuetype": {"name": "Bug"},
"summary": "color-contrast on /pricing",
"description": "Contrast 3.1:1 fails WCAG 1.4.3", # <-- rejected: not ADF
}
}
resp = requests.post(
"https://acme.atlassian.net/rest/api/3/issue",
json=payload, auth=("bot@acme.com", "API_TOKEN"),
)
resp.raise_for_status() # 400: description Operation value must be an Adf objectCorrect Implementation
Search by the fingerprint label first, build an ADF description body, and create only on a miss. The fingerprint lives in labels because Jira indexes labels for fast, exact JQL matching.
import requests
BASE = "https://acme.atlassian.net"
AUTH = ("bot@acme.com", "API_TOKEN") # service-account token, never a human's
SESSION = requests.Session()
SESSION.auth = AUTH
SESSION.headers.update({"Accept": "application/json", "Content-Type": "application/json"})
# Severity tier -> Jira priority name. Sourced from the severity/SLA guide.
PRIORITY = {"T1-critical": "Highest", "T2-serious": "High",
"T3-moderate": "Medium", "T4-minor": "Low"}
def find_by_fingerprint(fingerprint: str) -> str | None:
"""Exact-match JQL on the fingerprint label. Returns an issue key or None."""
jql = f'project = A11Y AND labels = "{fingerprint}" ORDER BY created DESC'
r = SESSION.post(f"{BASE}/rest/api/3/search",
json={"jql": jql, "maxResults": 1, "fields": ["status"]})
r.raise_for_status()
issues = r.json()["issues"]
return issues[0]["key"] if issues else None
def adf(finding: dict) -> dict:
"""Atlassian Document Format body — v3 rejects a plain description string."""
line = (f'{finding["rule_id"]} ({finding["wcag_sc"]}) at '
f'{finding["stable_target"]} on {finding["url"]}')
return {
"type": "doc", "version": 1,
"content": [{"type": "paragraph",
"content": [{"type": "text", "text": line}]}],
}
def create_or_update(finding: dict, fingerprint: str, tier: str, sla_due: str) -> str:
existing = find_by_fingerprint(fingerprint)
if existing:
# Idempotent: refresh only automation-owned fields, never the assignee.
SESSION.put(f"{BASE}/rest/api/3/issue/{existing}",
json={"update": {"labels": [{"add": f"seen-{finding['run_id']}"}]}})
return existing
payload = {
"fields": {
"project": {"key": "A11Y"},
"issuetype": {"name": "Bug"},
"summary": f'{finding["rule_id"]}: {finding["wcag_sc"]} on {finding["url"]}',
"description": adf(finding),
"priority": {"name": PRIORITY[tier]},
# Fingerprint as a label = the external dedup key JQL searches on.
# WCAG SC dotted form ("1.4.3") is label-safe once slugged.
"labels": [fingerprint, "a11y-automation", f'wcag-{finding["wcag_sc"].replace(".", "-")}'],
# SLA due date stamped at creation; Jira's built-in duedate field.
"duedate": sla_due[:10], # YYYY-MM-DD
}
}
r = SESSION.post(f"{BASE}/rest/api/3/issue", json=payload)
r.raise_for_status()
return r.json()["key"]Two details make this robust. The JQL uses labels = "fingerprint" for an exact indexed match rather than a ~ text search, which would fuzzily match unrelated issues. And the create sends duedate as a bare YYYY-MM-DD string — Jira’s system duedate field takes a date, not a datetime, so slicing the ISO SLA timestamp to ten characters is required.
Pipeline Integration Note
This binding is one leaf of the reconciliation loop. The parent router computes the fingerprint and decides create versus update versus close; it calls create_or_update for each finding in its plan and calls a transition helper for the close sweep. Findings arrive already deduplicated and owner-tagged from the error categorization and triage pipelines, and the priority/duedate values come from the tier model in severity tiers and SLA tracking for accessibility violations. Closing a resolved defect is a workflow transition, not a field write: resolve the target transition id from GET /rest/api/3/issue/{key}/transitions and POST to that endpoint, because Jira status changes go through the workflow engine rather than a direct status update.
Gotchas
- The create screen governs which fields you can set. A
priorityor custom field absent from the project’s create screen is rejected outright, not silently ignored. Add the field to the screen in project settings, or set it in a follow-upPUT/transition after creation rather than in the create payload. - Labels cannot contain spaces and are case-preserving. A fingerprint like
a11y-fp-9f2cis safe, but never let a raw selector or a space into a label — Jira truncates at the first space and the lookup silently stops matching. Keep the fingerprint alphanumeric-plus-hyphen. - JQL label search is eventually consistent. Immediately after creating an issue, a fingerprint search may not return it for a second or two while Jira’s index catches up. Within a single run this is a non-issue because you hold the returned key in memory; across rapidly retried runs, dedupe on the in-memory plan, not on a fresh JQL round-trip per finding.
Frequently Asked Questions
Why does Jira reject my description with an ADF error?
Jira Cloud REST API v3 requires the description as an Atlassian Document Format object, not a plain string. Wrap your text in a {"type": "doc", "version": 1, "content": [...]} structure with paragraph and text nodes. The older v2 API accepted a raw string, so code ported from v2 breaks here.
Should I store the fingerprint in a label or a custom field?
A label is simpler and indexed for exact JQL matching without any project configuration, which is why the examples use one. A dedicated custom field is cleaner if you have many labels competing for space or need typed reporting, but it requires creating the field and adding it to screens. Either works as the dedup key as long as your lookup queries it exactly.
How do I close a Jira issue when the violation no longer reproduces?
Do not write the status field directly — Jira routes status changes through its workflow engine. Fetch the available transitions for the issue, find the one leading to your Done or Resolved status, and POST its id to the transitions endpoint. The parent router triggers this for any open automated issue whose fingerprint is absent from the current scan.
Can I set the SLA due date on the standard duedate field?
Yes. Jira’s system duedate field accepts a YYYY-MM-DD date, so slice your ISO 8601 SLA timestamp to its first ten characters. If you need the full datetime or want breach automation, store the timestamp in a custom field as well and drive alerts off that; the standard field is date-granular only.