Skip to content

Commit 6b29bb3

Browse files
authored
TW-6413: fix for attendees and similar serialization as query params
1 parent fcc18bf commit 6b29bb3

8 files changed

Lines changed: 335 additions & 21 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
### Added
66
* Optional `TrackingOptions.domainName` support for custom link and open tracking hostnames in regular sends, Transactional Send, drafts, and scheduled sends. The field serializes as `tracking_options.domain_name` and is omitted when unset.
77

8+
### Fixed
9+
* `metadata_pair` (calendars, events, messages) is now sent as a single `key:value` query parameter using the first map entry, instead of one repeated parameter per entry. The API accepts exactly one pair, so maps with more than one entry previously had all but one entry silently ignored. Public types are unchanged (`Map<String, String>`); single-entry and empty maps produce the same request as before.
10+
* `to`, `from`, `cc`, `bcc`, and `in` (messages, threads, drafts) are now sent as a single query parameter using the first list entry, instead of as repeated parameters. The API binds each as a scalar string and validates `to`/`from`/`cc`/`bcc` as one email address, so repeated parameters meant the API silently kept just one value — whichever its parser happened to keep last. Sending one parameter makes that deterministic and matches the behavior `inFolder` already documented. Public types are unchanged (`List<String>`); passing a single-element list produces the same request as before. These parameters will change type to `String` in a future major version.
11+
* `attendees` (events) and `any_email` (messages, threads, drafts) are now sent as a single comma-delimited query parameter instead of repeated parameters. The API parses both as comma-delimited strings, so passing more than one value previously caused the API to keep only the last one, returning silently incorrect, order-dependent results. Public types are unchanged (`List<String>`), and null, empty, and single-element values produce the same request as before. Note that `any_email` accepts at most 25 addresses; over-long lists now surface an API validation error instead of silently filtering on a single address. `event_type` is unaffected — the API genuinely supports repeating it.
12+
813
## [v2.18.0] - Release 2026-07-10
914

1015
### Added

‎src/main/kotlin/com/nylas/NylasClient.kt‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -656,14 +656,16 @@ open class NylasClient(
656656
for ((key, value) in params) {
657657
when (value) {
658658
is List<*> -> {
659+
// Parameters the API does not accept as repeated values are already collapsed by
660+
// the query parameter class itself, via IQueryParams.convertToMap.
659661
for (item in value) {
660662
url.addQueryParameter(key, item.toString())
661663
}
662664
}
663665
is Map<*, *> -> {
664-
for ((k, v) in value) {
665-
url.addQueryParameter(key, "$k:$v")
666-
}
666+
// Only metadata_pair is a map, and the API accepts exactly one `key:value` pair —
667+
// repeating the parameter makes it keep just one and silently ignore the rest.
668+
value.entries.firstOrNull()?.let { (k, v) -> url.addQueryParameter(key, "$k:$v") }
667669
}
668670
is Double -> {
669671
url.addQueryParameter(key, value.toInt().toString())

‎src/main/kotlin/com/nylas/models/IQueryParams.kt‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,48 @@ interface IQueryParams {
2222
return JsonHelper.jsonMapAdapter.fromJson(json)!!
2323
}
2424
}
25+
26+
/**
27+
* Rewrite the named `List` query parameters, which the API does not accept as repeated
28+
* parameters. An empty list drops the parameter entirely; values that are not lists, and keys
29+
* that are absent, are left untouched.
30+
*
31+
* Each query parameter class declares its own affected keys, rather than [com.nylas.NylasClient]
32+
* holding a central list: parameter names are only meaningful per endpoint, and several of them
33+
* (`to`, `from`, `cc`, `bcc`) are also request *body* field names, where repeated values are
34+
* correct and collapsing them would silently drop recipients.
35+
*/
36+
private fun Map<String, Any>.rewriteListParams(
37+
keys: Array<out String>,
38+
rewrite: (List<*>) -> String,
39+
): Map<String, Any> {
40+
val rewritten = toMutableMap()
41+
for (key in keys) {
42+
val value = rewritten[key]
43+
if (value is List<*>) {
44+
if (value.isEmpty()) {
45+
rewritten.remove(key)
46+
} else {
47+
rewritten[key] = rewrite(value)
48+
}
49+
}
50+
}
51+
return rewritten
52+
}
53+
54+
/**
55+
* Join query parameters that the API parses as a single comma-delimited string. Repeating them
56+
* would make the API keep only one value and silently drop the rest.
57+
*/
58+
internal fun Map<String, Any>.joinCommaDelimitedParams(vararg keys: String): Map<String, Any> =
59+
rewriteListParams(keys) { list -> list.joinToString(",") }
60+
61+
/**
62+
* Send only the first entry of query parameters that the API accepts as a single value, but which
63+
* this SDK still types as `List<String>` for backwards compatibility.
64+
*
65+
* The API keeps exactly one value for these regardless; sending one parameter makes which value
66+
* wins deterministic instead of dependent on the API parser's repeated-parameter behavior.
67+
*/
68+
internal fun Map<String, Any>.collapseSingleValueParams(vararg keys: String): Map<String, Any> =
69+
rewriteListParams(keys) { list -> list.first().toString() }

‎src/main/kotlin/com/nylas/models/ListDraftsQueryParams.kt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,41 @@ data class ListDraftsQueryParams(
2525
val subject: String? = null,
2626
/**
2727
* Return emails that have been sent or received from this list of email addresses.
28+
* Pass one email address per list entry; the SDK sends them to the API as a single
29+
* comma-delimited value. The API accepts a maximum of 25 addresses.
2830
*/
2931
@Json(name = "any_email")
3032
val anyEmail: List<String>? = null,
3133
/**
3234
* Return items containing drafts to be sent these email address.
35+
*
36+
* Note: the API filters on a single address. If a list is provided, only the first
37+
* entry is used.
38+
*
39+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
40+
* String in a future major version.
3341
*/
3442
@Json(name = "to")
3543
val to: List<String>? = null,
3644
/**
3745
* Return items containing drafts cc'ing these email address.
46+
*
47+
* Note: the API filters on a single address. If a list is provided, only the first
48+
* entry is used.
49+
*
50+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
51+
* String in a future major version.
3852
*/
3953
@Json(name = "cc")
4054
val cc: List<String>? = null,
4155
/**
4256
* Return items containing drafts bcc'ing these email address.
57+
*
58+
* Note: the API filters on a single address. If a list is provided, only the first
59+
* entry is used.
60+
*
61+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
62+
* String in a future major version.
4363
*/
4464
@Json(name = "bcc")
4565
val bcc: List<String>? = null,
@@ -64,6 +84,16 @@ data class ListDraftsQueryParams(
6484
@Json(name = "has_attachment")
6585
val hasAttachment: Boolean? = null,
6686
) : IQueryParams {
87+
88+
/**
89+
* The API does not accept repeated values for these parameters.
90+
* See [joinCommaDelimitedParams] and [collapseSingleValueParams].
91+
*/
92+
override fun convertToMap(): Map<String, Any> =
93+
super.convertToMap()
94+
.collapseSingleValueParams("to", "cc", "bcc")
95+
.joinCommaDelimitedParams("any_email")
96+
6797
/**
6898
* Builder for [ListDraftsQueryParams].
6999
*/

‎src/main/kotlin/com/nylas/models/ListEventQueryParams.kt‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ data class ListEventQueryParams(
112112
val updatedAfter: Long? = null,
113113
/**
114114
* Filter for events that include the specified attendees.
115-
* This parameter accepts a comma-delimited list of email addresses.
115+
* Pass one email address per list entry; the SDK sends them to the API as a single
116+
* comma-delimited value.
117+
* Note: multiple attendees are matched with AND — an event must include every address
118+
* listed to be returned.
116119
* (Not supported for virtual calendars)
117120
*/
118121
@Json(name = "attendees")
@@ -135,6 +138,14 @@ data class ListEventQueryParams(
135138
@Json(name = "tentative_as_busy")
136139
val tentativeAsBusy: Boolean? = null,
137140
) : IQueryParams {
141+
142+
/**
143+
* The API does not accept repeated values for this parameter.
144+
* See [joinCommaDelimitedParams].
145+
*/
146+
override fun convertToMap(): Map<String, Any> =
147+
super.convertToMap().joinCommaDelimitedParams("attendees")
148+
138149
/**
139150
* Builder for [ListEventQueryParams].
140151
*/
@@ -295,7 +306,10 @@ data class ListEventQueryParams(
295306

296307
/**
297308
* Sets the attendees to filter for events with.
298-
* This parameter accepts a comma-delimited list of email addresses.
309+
* Pass one email address per list entry; the SDK sends them to the API as a single
310+
* comma-delimited value.
311+
* Note: multiple attendees are matched with AND — an event must include every address
312+
* listed to be returned.
299313
* (Not supported for virtual calendars)
300314
* @param attendees The attendees to filter for events with.
301315
* @return The builder.

‎src/main/kotlin/com/nylas/models/ListMessagesQueryParams.kt‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,63 @@ data class ListMessagesQueryParams(
2525
val subject: String? = null,
2626
/**
2727
* Return emails that have been sent or received from this list of email addresses.
28+
* Pass one email address per list entry; the SDK sends them to the API as a single
29+
* comma-delimited value. The API accepts a maximum of 25 addresses.
2830
*/
2931
@Json(name = "any_email")
3032
val anyEmail: List<String>? = null,
3133
/**
3234
* Return items containing messages sent to these email address.
35+
*
36+
* Note: the API filters on a single address. If a list is provided, only the first
37+
* entry is used.
38+
*
39+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
40+
* String in a future major version.
3341
*/
3442
@Json(name = "to")
3543
val to: List<String>? = null,
3644
/**
3745
* Return items containing messages sent from these email address.
46+
*
47+
* Note: the API filters on a single address. If a list is provided, only the first
48+
* entry is used.
49+
*
50+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
51+
* String in a future major version.
3852
*/
3953
@Json(name = "from")
4054
val from: List<String>? = null,
4155
/**
4256
* Return items containing messages cc'd on these email address.
57+
*
58+
* Note: the API filters on a single address. If a list is provided, only the first
59+
* entry is used.
60+
*
61+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
62+
* String in a future major version.
4363
*/
4464
@Json(name = "cc")
4565
val cc: List<String>? = null,
4666
/**
4767
* Return items containing messages bcc'd on these email address.
68+
*
69+
* Note: the API filters on a single address. If a list is provided, only the first
70+
* entry is used.
71+
*
72+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
73+
* String in a future major version.
4874
*/
4975
@Json(name = "bcc")
5076
val bcc: List<String>? = null,
5177
/**
5278
* Return emails that are in these folder IDs.
79+
*
80+
* Note: the API filters on a single folder or label ID. If a list is provided, only the first
81+
* entry is used.
82+
*
83+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
84+
* String in a future major version.
5385
*/
5486
@Json(name = "in")
5587
val inFolder: List<String>? = null,
@@ -100,6 +132,16 @@ data class ListMessagesQueryParams(
100132
@Json(name = "metadata_pair")
101133
val metadataPair: Map<String, String>? = null,
102134
) : IQueryParams {
135+
136+
/**
137+
* The API does not accept repeated values for these parameters.
138+
* See [joinCommaDelimitedParams] and [collapseSingleValueParams].
139+
*/
140+
override fun convertToMap(): Map<String, Any> =
141+
super.convertToMap()
142+
.collapseSingleValueParams("to", "from", "cc", "bcc", "in")
143+
.joinCommaDelimitedParams("any_email")
144+
103145
class Builder {
104146
private var limit: Int? = null
105147
private var pageToken: String? = null

‎src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt‎

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,52 @@ data class ListThreadsQueryParams(
2222
val subject: String? = null,
2323
/**
2424
* Return emails that have been sent or received from this list of email addresses.
25+
* Pass one email address per list entry; the SDK sends them to the API as a single
26+
* comma-delimited value. The API accepts a maximum of 25 addresses.
2527
*/
2628
@Json(name = "any_email")
2729
val anyEmail: List<String>? = null,
2830
/**
2931
* Return items containing messages sent to these email address.
32+
*
33+
* Note: the API filters on a single address. If a list is provided, only the first
34+
* entry is used.
35+
*
36+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
37+
* String in a future major version.
3038
*/
3139
@Json(name = "to")
3240
val to: List<String>? = null,
3341
/**
3442
* Return items containing messages sent from these email address.
43+
*
44+
* Note: the API filters on a single address. If a list is provided, only the first
45+
* entry is used.
46+
*
47+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
48+
* String in a future major version.
3549
*/
3650
@Json(name = "from")
3751
val from: List<String>? = null,
3852
/**
3953
* Return items containing messages cc'd on these email address.
54+
*
55+
* Note: the API filters on a single address. If a list is provided, only the first
56+
* entry is used.
57+
*
58+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
59+
* String in a future major version.
4060
*/
4161
@Json(name = "cc")
4262
val cc: List<String>? = null,
4363
/**
4464
* Return items containing messages bcc'd on these email address.
65+
*
66+
* Note: the API filters on a single address. If a list is provided, only the first
67+
* entry is used.
68+
*
69+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to
70+
* String in a future major version.
4571
*/
4672
@Json(name = "bcc")
4773
val bcc: List<String>? = null,
@@ -91,22 +117,13 @@ data class ListThreadsQueryParams(
91117
) : IQueryParams {
92118

93119
/**
94-
* Override convertToMap to handle the inFolder parameter correctly.
95-
* The API expects a single folder ID, so we use only the first item if a list is provided.
120+
* The API does not accept repeated values for these parameters.
121+
* See [joinCommaDelimitedParams] and [collapseSingleValueParams].
96122
*/
97-
override fun convertToMap(): Map<String, Any> {
98-
val map = super.convertToMap().toMutableMap()
99-
100-
// Handle inFolder parameter to use only the first item if it's a list
101-
if (inFolder?.isNotEmpty() == true) {
102-
map["in"] = inFolder.first()
103-
} else if (inFolder?.isEmpty() == true) {
104-
// Remove the "in" key if the list is empty
105-
map.remove("in")
106-
}
107-
108-
return map
109-
}
123+
override fun convertToMap(): Map<String, Any> =
124+
super.convertToMap()
125+
.collapseSingleValueParams("to", "from", "cc", "bcc", "in")
126+
.joinCommaDelimitedParams("any_email")
110127

111128
/**
112129
* Builder for [ListThreadsQueryParams].

0 commit comments

Comments
 (0)