Skip to main content

devboy_jira/
types.rs

1//! Jira API response types.
2//!
3//! These types represent the raw JSON responses from Jira API v2/v3.
4//! They are deserialized and then mapped to unified types. Most are
5//! internal — only `JiraField` and `JiraFieldSchema` are re-exported
6//! at the crate root for downstream consumers.
7//!
8//! `dead_code` is allowed at the module level because some fields
9//! exist purely so serde captures them off the wire (for forward
10//! compatibility with logs and debug dumps) without the mapper
11//! reading them yet. They are not unused — they pin the JSON shape
12//! the API returns and surface in `Debug` impls.
13
14#![allow(dead_code)]
15
16use serde::{Deserialize, Serialize};
17
18// =============================================================================
19// User
20// =============================================================================
21
22#[derive(Debug, Clone, Deserialize)]
23pub struct JiraUser {
24    /// Account ID (Cloud only)
25    #[serde(default, rename = "accountId")]
26    pub account_id: Option<String>,
27    /// Username (Self-Hosted only)
28    #[serde(default)]
29    pub name: Option<String>,
30    #[serde(default, rename = "displayName")]
31    pub display_name: Option<String>,
32    #[serde(default, rename = "emailAddress")]
33    pub email_address: Option<String>,
34}
35
36// =============================================================================
37// Issue
38// =============================================================================
39
40#[derive(Debug, Clone, Deserialize)]
41pub struct JiraIssue {
42    pub id: String,
43    /// Issue key (e.g., "PROJ-123")
44    pub key: String,
45    pub fields: JiraIssueFields,
46}
47
48#[derive(Debug, Clone, Deserialize)]
49pub struct JiraIssueFields {
50    #[serde(default)]
51    pub summary: Option<String>,
52    /// Description — plain text (v2) or ADF document (v3)
53    #[serde(default)]
54    pub description: Option<serde_json::Value>,
55    #[serde(default)]
56    pub status: Option<JiraStatus>,
57    #[serde(default)]
58    pub priority: Option<JiraPriority>,
59    #[serde(default)]
60    pub assignee: Option<JiraUser>,
61    /// Reporter (author)
62    #[serde(default)]
63    pub reporter: Option<JiraUser>,
64    #[serde(default)]
65    pub labels: Vec<String>,
66    /// Created timestamp
67    #[serde(default)]
68    pub created: Option<String>,
69    /// Updated timestamp
70    #[serde(default)]
71    pub updated: Option<String>,
72    /// Issue type reference. Read-only — captures `issuetype.name` so
73    /// callers can branch on `"Epic"` / `"Story"` / `"Sub-task"` without
74    /// re-parsing the raw payload.
75    #[serde(default)]
76    pub issuetype: Option<JiraIssueTypeRef>,
77    /// Parent issue (for subtasks)
78    #[serde(default)]
79    pub parent: Option<Box<JiraIssue>>,
80    /// Subtasks / child issues
81    #[serde(default)]
82    pub subtasks: Vec<JiraIssue>,
83    #[serde(default)]
84    pub issuelinks: Vec<JiraIssueLink>,
85    /// Attachments on the issue (present when the caller requests
86    /// `fields=attachment` or uses `fields=*all`).
87    #[serde(default)]
88    pub attachment: Vec<JiraAttachment>,
89    /// Catch-all for everything else Jira returns under `fields` —
90    /// most importantly the instance-specific `customfield_*` slots.
91    /// Lets the mapper read e.g. an Epic Description customfield
92    /// without forcing every customfield to be modelled as a typed
93    /// field.
94    #[serde(flatten, default)]
95    pub extras: std::collections::HashMap<String, serde_json::Value>,
96}
97
98/// Lightweight read-only reference to an issue type — only the name
99/// is captured because that's what mapping logic branches on.
100#[derive(Debug, Clone, Deserialize)]
101pub struct JiraIssueTypeRef {
102    pub name: String,
103}
104
105/// Jira attachment as returned inside `fields.attachment`.
106#[derive(Debug, Clone, Deserialize)]
107pub struct JiraAttachment {
108    /// Attachment id (numeric string).
109    pub id: String,
110    #[serde(default)]
111    pub filename: Option<String>,
112    /// Direct download URL (`content` in the Jira API).
113    #[serde(default)]
114    pub content: Option<String>,
115    #[serde(default)]
116    pub size: Option<u64>,
117    #[serde(default, rename = "mimeType")]
118    pub mime_type: Option<String>,
119    /// Creation timestamp (ISO 8601).
120    #[serde(default)]
121    pub created: Option<String>,
122    /// Author — Jira uses `author` inside the attachment object.
123    #[serde(default)]
124    pub author: Option<JiraUser>,
125}
126
127#[derive(Debug, Clone, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct JiraStatus {
130    pub name: String,
131    /// Status category (new, indeterminate, done)
132    #[serde(default)]
133    pub status_category: Option<JiraStatusCategory>,
134}
135
136#[derive(Debug, Clone, Deserialize)]
137pub struct JiraStatusCategory {
138    /// Category key: "new", "indeterminate", "done"
139    pub key: String,
140}
141
142#[derive(Debug, Clone, Deserialize)]
143pub struct JiraPriority {
144    pub name: String,
145}
146
147// =============================================================================
148// Issue Links
149// =============================================================================
150
151#[derive(Debug, Clone, Deserialize)]
152pub struct JiraIssueLink {
153    /// Link ID
154    #[serde(default)]
155    pub id: Option<String>,
156    #[serde(rename = "type")]
157    pub link_type: JiraIssueLinkType,
158    /// Inward issue (e.g., "is blocked by" this issue)
159    #[serde(default, rename = "inwardIssue")]
160    pub inward_issue: Option<Box<JiraIssue>>,
161    /// Outward issue (e.g., "blocks" this issue)
162    #[serde(default, rename = "outwardIssue")]
163    pub outward_issue: Option<Box<JiraIssue>>,
164}
165
166#[derive(Debug, Clone, Deserialize)]
167pub struct JiraIssueLinkType {
168    /// Link type name (e.g., "Blocks", "Relates")
169    pub name: String,
170    /// Inward description (e.g., "is blocked by")
171    #[serde(default)]
172    pub inward: Option<String>,
173    /// Outward description (e.g., "blocks")
174    #[serde(default)]
175    pub outward: Option<String>,
176}
177
178// =============================================================================
179// Search Response
180// =============================================================================
181
182/// Search response from Self-Hosted Jira (API v2, GET /search).
183#[derive(Debug, Clone, Deserialize)]
184pub struct JiraSearchResponse {
185    pub issues: Vec<JiraIssue>,
186    /// Starting index
187    #[serde(default, rename = "startAt")]
188    pub start_at: Option<u32>,
189    /// Max results per page
190    #[serde(default, rename = "maxResults")]
191    pub max_results: Option<u32>,
192    /// Total number of results
193    #[serde(default)]
194    pub total: Option<u32>,
195}
196
197/// Search response from Jira Cloud (API v3, GET /search/jql).
198#[derive(Debug, Clone, Deserialize)]
199pub struct JiraCloudSearchResponse {
200    pub issues: Vec<JiraIssue>,
201    #[serde(default, rename = "nextPageToken")]
202    pub next_page_token: Option<String>,
203}
204
205// =============================================================================
206// Comment
207// =============================================================================
208
209/// Jira comment representation.
210#[derive(Debug, Clone, Deserialize)]
211pub struct JiraComment {
212    /// Comment ID
213    pub id: String,
214    /// Comment body — plain text (v2) or ADF document (v3)
215    #[serde(default)]
216    pub body: Option<serde_json::Value>,
217    /// Comment author
218    #[serde(default)]
219    pub author: Option<JiraUser>,
220    /// Created timestamp
221    #[serde(default)]
222    pub created: Option<String>,
223    /// Updated timestamp
224    #[serde(default)]
225    pub updated: Option<String>,
226}
227
228/// Response from GET /issue/{key}/comment.
229#[derive(Debug, Clone, Deserialize)]
230pub struct JiraCommentsResponse {
231    pub comments: Vec<JiraComment>,
232}
233
234// =============================================================================
235// Transitions
236// =============================================================================
237
238/// Jira transition representation.
239#[derive(Debug, Clone, Deserialize)]
240pub struct JiraTransition {
241    /// Transition ID
242    pub id: String,
243    /// Transition name
244    pub name: String,
245    /// Target status
246    pub to: JiraStatus,
247}
248
249/// Response from GET /issue/{key}/transitions.
250#[derive(Debug, Clone, Deserialize)]
251pub struct JiraTransitionsResponse {
252    /// Available transitions
253    pub transitions: Vec<JiraTransition>,
254}
255
256// =============================================================================
257// Create/Update types
258// =============================================================================
259
260/// Request body for creating an issue.
261#[derive(Debug, Clone, Serialize)]
262pub struct CreateIssuePayload {
263    /// Issue fields
264    pub fields: CreateIssueFields,
265}
266
267/// Fields for creating an issue.
268#[derive(Debug, Clone, Serialize)]
269pub struct CreateIssueFields {
270    pub project: ProjectKey,
271    /// Summary (title)
272    pub summary: String,
273    /// Issue type
274    pub issuetype: IssueType,
275    /// Description — plain text (v2) or ADF (v3)
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub description: Option<serde_json::Value>,
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub labels: Option<Vec<String>>,
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub priority: Option<PriorityName>,
282    #[serde(skip_serializing_if = "Option::is_none")]
283    pub assignee: Option<serde_json::Value>,
284    /// Components (issue #197).
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub components: Option<Vec<ComponentRef>>,
287    /// Fix versions / release targets. Serialised as
288    /// `[{ "name": "..." }, ...]` into Jira's `fields.fixVersions`.
289    #[serde(rename = "fixVersions", skip_serializing_if = "Option::is_none")]
290    pub fix_versions: Option<Vec<VersionRef>>,
291    /// Parent issue reference. Required by Jira when `issuetype` is a
292    /// sub-task or any "is_subtask" hierarchical type — the API rejects
293    /// the request with a 400 otherwise (issue #214). Serialised as
294    /// `{ "key": "PROJ-1" }`, matching Jira's `fields.parent` shape.
295    /// Uses [`IssueKeyRef`] (not [`ProjectKey`]) because the value is an
296    /// **issue** key (e.g. `PROJ-1`), not a project key (e.g. `PROJ`).
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub parent: Option<IssueKeyRef>,
299}
300
301/// Component reference used in create/update issue payloads (issue #197).
302///
303/// We serialise by **name** (`{ "name": "..." }`) to line up with the
304/// Jira schema enricher, which enumerates component *names* in tool
305/// schemas (`JiraComponent.name`). Jira accepts either `{id}` or `{name}`
306/// in `fields.components`, so name-based references work across Cloud +
307/// Self-Hosted without forcing callers to resolve ids first.
308///
309/// This is addressed in Copilot review on PR #205.
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct ComponentRef {
312    pub name: String,
313}
314
315/// Fix-version reference used in create/update issue payloads.
316///
317/// Same shape and rationale as [`ComponentRef`]: name-based reference
318/// that works across Cloud and Self-Hosted without callers having to
319/// resolve numeric ids first. Pairs with the `fix_versions` field in
320/// [`devboy_core::CreateIssueInput`] / [`devboy_core::UpdateIssueInput`].
321#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct VersionRef {
323    pub name: String,
324}
325
326/// Project key reference.
327#[derive(Debug, Clone, Serialize)]
328pub struct ProjectKey {
329    /// Project key (e.g., "PROJ")
330    pub key: String,
331}
332
333/// Issue type reference.
334#[derive(Debug, Clone, Serialize)]
335pub struct IssueType {
336    /// Issue type name
337    pub name: String,
338}
339
340/// Priority name reference.
341#[derive(Debug, Clone, Serialize)]
342pub struct PriorityName {
343    /// Priority name
344    pub name: String,
345}
346
347/// Request body for updating an issue.
348#[derive(Debug, Clone, Serialize)]
349pub struct UpdateIssuePayload {
350    /// Issue fields to update
351    pub fields: UpdateIssueFields,
352}
353
354/// Fields for updating an issue.
355#[derive(Debug, Clone, Serialize, Default)]
356pub struct UpdateIssueFields {
357    /// Summary (title)
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub summary: Option<String>,
360    /// Description — plain text (v2) or ADF (v3)
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub description: Option<serde_json::Value>,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub labels: Option<Vec<String>>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub priority: Option<PriorityName>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub assignee: Option<serde_json::Value>,
369    /// Components (issue #197). `None` means do not touch.
370    /// `Some(vec![])` clears all components.
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub components: Option<Vec<ComponentRef>>,
373    /// Fix versions. `None` leaves them untouched. `Some(vec![])` clears
374    /// all fix versions. `Some(vec![...])` replaces with the given
375    /// release names.
376    #[serde(rename = "fixVersions", skip_serializing_if = "Option::is_none")]
377    pub fix_versions: Option<Vec<VersionRef>>,
378}
379
380/// Request body for transitioning an issue.
381#[derive(Debug, Clone, Serialize)]
382pub struct TransitionPayload {
383    /// Transition to execute
384    pub transition: TransitionId,
385}
386
387/// Transition ID reference.
388#[derive(Debug, Clone, Serialize)]
389pub struct TransitionId {
390    /// Transition ID
391    pub id: String,
392}
393
394/// Response from POST /issue (create issue).
395#[derive(Debug, Clone, Deserialize)]
396pub struct CreateIssueResponse {
397    /// Issue ID
398    pub id: String,
399    /// Issue key (e.g., "PROJ-123")
400    pub key: String,
401}
402
403/// Request body for adding a comment.
404#[derive(Debug, Clone, Serialize)]
405pub struct AddCommentPayload {
406    /// Comment body — plain text (v2) or ADF (v3)
407    pub body: serde_json::Value,
408}
409
410// =============================================================================
411// Field discovery
412// =============================================================================
413
414/// Entry from `GET /rest/api/{v}/field` — every field (system + custom)
415/// available on the Jira instance.
416///
417/// Used to resolve human-readable field names (e.g. `"Epic Link"`,
418/// `"Sprint"`, `"Epic Name"`) to their numeric `customfield_*` ids,
419/// which vary across instances. Cached inside [`crate::JiraClient`] to
420/// avoid repeating the request for every issue mutation.
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct JiraField {
423    /// Field id, e.g. `"customfield_10014"` for customs or `"summary"`
424    /// for system fields.
425    pub id: String,
426    /// Human-readable name, e.g. `"Epic Link"`.
427    pub name: String,
428    /// Whether this is a custom field (`true`) or a system field
429    /// (`false`).
430    #[serde(default)]
431    pub custom: bool,
432    /// Optional schema descriptor.
433    #[serde(default)]
434    pub schema: Option<JiraFieldSchema>,
435}
436
437/// `schema` block of a [`JiraField`] entry. All fields optional —
438/// shape varies between system and custom fields and across Jira
439/// flavors.
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct JiraFieldSchema {
442    /// Top-level type, e.g. `"string"`, `"array"`, `"any"`.
443    #[serde(default, rename = "type")]
444    pub field_type: Option<String>,
445    /// Element type when `field_type == "array"`.
446    #[serde(default)]
447    pub items: Option<String>,
448    /// Custom field type URI, e.g.
449    /// `"com.pyxis.greenhopper.jira:gh-epic-link"`.
450    #[serde(default)]
451    pub custom: Option<String>,
452    /// Numeric custom field id (when `custom == true`).
453    #[serde(default, rename = "customId")]
454    pub custom_id: Option<i64>,
455    /// System field name when this is a system field.
456    #[serde(default)]
457    pub system: Option<String>,
458}
459
460// =============================================================================
461// Project Statuses
462// =============================================================================
463
464/// Response from GET /project/{key}/statuses.
465/// Returns statuses grouped by issue type.
466#[derive(Debug, Clone, Deserialize)]
467pub struct JiraIssueTypeStatuses {
468    /// Issue type name (e.g., "Task", "Bug")
469    #[serde(default)]
470    pub name: Option<String>,
471    /// Statuses available for this issue type
472    #[serde(default)]
473    pub statuses: Vec<JiraProjectStatus>,
474}
475
476/// A status within a project, including its category.
477#[derive(Debug, Clone, Deserialize)]
478#[serde(rename_all = "camelCase")]
479pub struct JiraProjectStatus {
480    /// Status name
481    pub name: String,
482    /// Status ID
483    #[serde(default)]
484    pub id: Option<String>,
485    #[serde(default)]
486    pub status_category: Option<JiraStatusCategory>,
487}
488
489// =============================================================================
490// Issue Link types
491// =============================================================================
492
493/// Request body for creating an issue link.
494#[derive(Debug, Clone, Serialize)]
495#[serde(rename_all = "camelCase")]
496pub struct CreateIssueLinkPayload {
497    #[serde(rename = "type")]
498    pub link_type: IssueLinkTypeName,
499    /// Inward issue (target)
500    pub inward_issue: IssueKeyRef,
501    /// Outward issue (source)
502    pub outward_issue: IssueKeyRef,
503}
504
505/// Issue link type name reference.
506#[derive(Debug, Clone, Serialize)]
507pub struct IssueLinkTypeName {
508    /// Link type name (e.g., "Blocks", "Relates")
509    pub name: String,
510}
511
512/// Issue key reference for linking.
513#[derive(Debug, Clone, Serialize)]
514pub struct IssueKeyRef {
515    /// Issue key (e.g., "PROJ-123")
516    pub key: String,
517}
518
519// =============================================================================
520// Jira Structure Plugin API types (/rest/structure/2.0/)
521// =============================================================================
522
523/// Structure info from GET /rest/structure/2.0/structure
524#[derive(Debug, Clone, Deserialize)]
525pub struct JiraStructure {
526    pub id: u64,
527    pub name: String,
528    #[serde(default)]
529    pub description: Option<String>,
530}
531
532/// Response from GET /rest/structure/2.0/structure
533#[derive(Debug, Clone, Deserialize)]
534pub struct JiraStructureListResponse {
535    pub structures: Vec<JiraStructure>,
536}
537
538/// A single row in the forest (compact format from API)
539#[derive(Debug, Clone, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct JiraForestRow {
542    pub id: u64,
543    #[serde(default)]
544    pub item_id: Option<String>,
545    #[serde(default)]
546    pub item_type: Option<String>,
547}
548
549/// Forest response from POST /rest/structure/2.0/forest/{id}/spec
550#[derive(Debug, Clone, Deserialize)]
551#[serde(rename_all = "camelCase")]
552pub struct JiraForestResponse {
553    pub version: u64,
554    #[serde(default)]
555    pub rows: Vec<JiraForestRow>,
556    #[serde(default)]
557    pub depths: Vec<u32>,
558    #[serde(default)]
559    pub total_count: Option<u64>,
560}
561
562/// Response from forest modification operations (add/move)
563#[derive(Debug, Clone, Deserialize)]
564pub struct JiraForestModifyResponse {
565    pub version: u64,
566    #[serde(default)]
567    pub rows: Vec<JiraForestRow>,
568}
569
570/// Structure view from /rest/structure/2.0/view
571#[derive(Debug, Clone, Deserialize)]
572#[serde(rename_all = "camelCase")]
573pub struct JiraStructureView {
574    pub id: u64,
575    pub name: String,
576    /// Owning structure id. Left non-optional and **without**
577    /// `#[serde(default)]` so a missing / renamed field from the API
578    /// fails loudly rather than silently deserialising to `0` — the
579    /// caller uses this id for cross-structure scope checks.
580    pub structure_id: u64,
581    #[serde(default)]
582    pub columns: Vec<JiraStructureViewColumn>,
583    #[serde(default)]
584    pub group_by: Option<String>,
585    #[serde(default)]
586    pub sort_by: Option<String>,
587    #[serde(default)]
588    pub filter: Option<String>,
589}
590
591/// Column definition in a structure view
592#[derive(Debug, Clone, Deserialize, Serialize)]
593pub struct JiraStructureViewColumn {
594    #[serde(default)]
595    pub id: Option<String>,
596    #[serde(default)]
597    pub field: Option<String>,
598    #[serde(default)]
599    pub formula: Option<String>,
600    #[serde(default)]
601    pub width: Option<u32>,
602}
603
604/// Response from GET /rest/structure/2.0/view?structureId={id}
605#[derive(Debug, Clone, Deserialize)]
606pub struct JiraStructureViewListResponse {
607    pub views: Vec<JiraStructureView>,
608}
609
610/// Batch value response from POST /rest/structure/2.0/value
611#[derive(Debug, Clone, Deserialize)]
612#[serde(rename_all = "camelCase")]
613pub struct JiraStructureValueEntry {
614    pub row_id: u64,
615    #[serde(default)]
616    pub column_id: Option<String>,
617    #[serde(default)]
618    pub value: serde_json::Value,
619}
620
621/// Response from POST /rest/structure/2.0/value
622#[derive(Debug, Clone, Deserialize)]
623pub struct JiraStructureValuesResponse {
624    pub values: Vec<JiraStructureValueEntry>,
625}
626
627// =============================================================================
628// Jira Project Versions (issue #238) — /rest/api/2/version + /project/{key}/versions
629// =============================================================================
630
631/// Version DTO returned by the Jira REST API.
632///
633/// Field set is the union of Cloud (v3) and Server/DC (v2) — both use
634/// the same path family. `issuesStatusForFixVersion` only appears when
635/// the caller passes `?expand=issuesstatus`.
636#[derive(Debug, Clone, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct JiraVersionDto {
639    pub id: String,
640    pub name: String,
641    /// Project key (e.g., "PROJ"). Server returns this directly; Cloud
642    /// returns `projectId` separately, so we accept either.
643    #[serde(default)]
644    pub project: Option<String>,
645    #[serde(default)]
646    pub project_id: Option<u64>,
647    #[serde(default)]
648    pub description: Option<String>,
649    #[serde(default)]
650    pub start_date: Option<String>,
651    #[serde(default)]
652    pub release_date: Option<String>,
653    #[serde(default)]
654    pub released: bool,
655    #[serde(default)]
656    pub archived: bool,
657    /// Computed by the server: true when releaseDate is in the past
658    /// and the version is still unreleased.
659    #[serde(default)]
660    pub overdue: Option<bool>,
661    /// Returned only with `?expand=issuesstatus` (Cloud) — keys are
662    /// status categories (`unmapped`, `toDo`, `inProgress`, `done`).
663    #[serde(default)]
664    pub issues_status_for_fix_version: Option<JiraVersionIssueStatusCounts>,
665    /// Server/DC returns this directly on the version DTO. Callers that
666    /// need a total fixed-issue count have to hit
667    /// `/version/{id}/relatedIssueCounts` separately.
668    #[serde(default)]
669    pub issues_unresolved_count: Option<u32>,
670}
671
672/// Issue counts by status category (Cloud `?expand=issuesstatus`).
673#[derive(Debug, Clone, Deserialize, Default)]
674#[serde(rename_all = "camelCase")]
675pub struct JiraVersionIssueStatusCounts {
676    #[serde(default)]
677    pub unmapped: u32,
678    #[serde(default)]
679    pub to_do: u32,
680    #[serde(default)]
681    pub in_progress: u32,
682    #[serde(default)]
683    pub done: u32,
684}
685
686impl JiraVersionIssueStatusCounts {
687    pub fn total(&self) -> u32 {
688        self.unmapped
689            .saturating_add(self.to_do)
690            .saturating_add(self.in_progress)
691            .saturating_add(self.done)
692    }
693}
694
695/// POST /rest/api/2/version payload.
696///
697/// `project` and `project_id` are mutually-exclusive routing keys —
698/// callers should fill exactly one (Server/DC accepts both, Cloud
699/// historically prefers `projectId`). Optional date / state fields
700/// are skipped when `None` so creation defaults match the UI.
701#[derive(Debug, Clone, Serialize, Default)]
702#[serde(rename_all = "camelCase")]
703pub struct CreateVersionPayload {
704    pub name: String,
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub project: Option<String>,
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub project_id: Option<u64>,
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub description: Option<String>,
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub start_date: Option<String>,
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub release_date: Option<String>,
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub released: Option<bool>,
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub archived: Option<bool>,
719}
720
721/// PUT /rest/api/2/version/{id} payload — partial update; only fields
722/// explicitly set are sent (`#[serde(skip_serializing_if = "Option::is_none")]`),
723/// so unspecified fields are preserved server-side.
724#[derive(Debug, Clone, Serialize, Default)]
725#[serde(rename_all = "camelCase")]
726pub struct UpdateVersionPayload {
727    #[serde(skip_serializing_if = "Option::is_none")]
728    pub name: Option<String>,
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub description: Option<String>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub start_date: Option<String>,
733    #[serde(skip_serializing_if = "Option::is_none")]
734    pub release_date: Option<String>,
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub released: Option<bool>,
737    #[serde(skip_serializing_if = "Option::is_none")]
738    pub archived: Option<bool>,
739}