Repository navigation
attendees (and similar fields) serialized as repeated query params, but API expects comma-delimited string — silently wrong, order-dependent results #337
Description
Summary
ListEventQueryParams.attendees is typed as List<String> and serialized by NylasClient.addQueryParams as repeated query params (attendees=a&attendees=b). But the field's own docstring says the API expects a comma-delimited string, and the actual /v3/grants/{grant_id}/events endpoint documents attendees as type string, not an array. There is no array type for attendees in the API contract at all.
Because of this mismatch, calling the SDK with multiple attendees silently returns wrong results — with no error, exception, or warning.
Where
- Field:
nylas-java/src/main/kotlin/com/nylas/models/ListEventQueryParams.kt
Lines 113 to 119 in fcc18bf
/** * Filter for events that include the specified attendees. * This parameter accepts a comma-delimited list of email addresses. * (Not supported for virtual calendars) */ @Json(name = "attendees") val attendees: List<String>? = null, /** * Filter for events that include the specified attendees. * This parameter accepts a comma-delimited list of email addresses. * (Not supported for virtual calendars) */ @Json(name = "attendees") val attendees: List<String>? = null,
- Serialization:
nylas-java/src/main/kotlin/com/nylas/NylasClient.kt
Lines 653 to 663 in fcc18bf
} private fun addQueryParams(url: HttpUrl.Builder, params: Map<String, Any>): HttpUrl.Builder { for ((key, value) in params) { when (value) { is List<*> -> { for (item in value) { url.addQueryParameter(key, item.toString()) } } is Map<*, *> -> { This emits oneis List<*> -> { for (item in value) { url.addQueryParameter(key, item.toString()) } }
attendees=param per list item instead of joining them into the single comma-delimited string the docstring (and the API docs) describe.
Reproduction
Tested directly against /v3/grants/{grant_id}/events (bypassing the SDK, to isolate server vs. client behavior) with 3 events in range: one with only peter, one with rumit+peter, one with ale+rumit+peter.
| Query string sent | Events returned |
|---|---|
attendees=ale@x.com&attendees=rumit@x.com |
2 (matches rumit-only filter — last param wins) |
attendees=rumit@x.com&attendees=ale@x.com (same values, order swapped) |
1 (matches ale-only filter — last param wins, not order-independent) |
attendees=ale@x.com,rumit@x.com (comma-joined, single param) |
1 (event with both — comma-join is AND, not "any of") |
So passing the SDK's attendees list currently: (a) silently drops every value except the last one, and (b) is order-dependent — the exact same logical filter returns a different result depending on the order items were added to the list, which is a pretty surprising property for a library caller to hit.
Suggested fix
Rather than fixing this one field at a time with a bespoke convertToMap() override per class (the pattern already used ad hoc for inFolder in ListThreadsQueryParams), it'd be worth adding a small piece of shared, declarative metadata to IQueryParams so every affected field opts in with one line, right next to its existing "comma-delimited" docstring:
interface IQueryParams {
/**
* Keys whose List<String> values should be serialized as a single
* comma-joined query param instead of one repeated param per item.
* Only needed for fields the API documents as a scalar comma-delimited
* string (e.g. attendees) — NOT for fields that genuinely support
* repeated params for OR semantics (e.g. event_type).
*/
val commaDelimitedKeys: Set<String> get() = emptySet()
fun convertToMap(): Map<String, Any> {
val json = JsonHelper.moshi().adapter(this.javaClass).toJson(this)
if (json.isEmpty()) return emptyMap()
val map = JsonHelper.jsonMapAdapter.fromJson(json)!!.toMutableMap()
for (key in commaDelimitedKeys) {
(map[key] as? List<*>)?.let { map[key] = it.joinToString(",") }
}
return map
}
}// ListEventQueryParams.kt
override val commaDelimitedKeys = setOf("attendees")This keeps the public field typed as List<String>? (good ergonomics — callers still just pass a list, no manual .joinToString(",") at every call site), doesn't touch addQueryParams at all, and replaces the existing inFolder-only special case in ListThreadsQueryParams with the same general mechanism. Any other field documented as a scalar comma-delimited string (to/cc/bcc/any_email on ListMessagesQueryParams/ListThreadsQueryParams) could opt in the same way.
Note: event_type on ListEventQueryParams should NOT be added to commaDelimitedKeys — its docs explicitly document repeated-param OR semantics with a worked example (event_type=default&event_type=outOfOffice → returns events that are default OR outOfOffice), so the current repeated-key serialization via addQueryParams's generic is List<*> -> branch is correct there. This issue is specifically about fields documented as a single comma-delimited string value, where that generic branch doesn't match what the field's own docs promise.
Impact
Any caller passing more than one value to attendees (or the affected Messages/Threads fields) gets a silently wrong, order-dependent result set with no indication anything went wrong — this seems worth a fix or at least a docstring correction + loud deprecation warning (as was done for inFolder) so callers aren't surprised.
Activity
googsvg commentedon Aug 12, 2026
thank you for reporting this @ryansmartpadpro , a great call ❤️
will be fixed and deployed via PR: #338