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:
|
/** |
|
* 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:
|
} |
|
|
|
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<*, *> -> { |
is List<*> -> {
for (item in value) {
url.addQueryParameter(key, item.toString())
}
}
This emits one 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.
Summary
ListEventQueryParams.attendeesis typed asList<String>and serialized byNylasClient.addQueryParamsas 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}/eventsendpoint documentsattendeesas typestring, not an array. There is no array type forattendeesin 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
nylas-java/src/main/kotlin/com/nylas/models/ListEventQueryParams.kt
Lines 113 to 119 in fcc18bf
nylas-java/src/main/kotlin/com/nylas/NylasClient.kt
Lines 653 to 663 in fcc18bf
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 onlypeter, one withrumit+peter, one withale+rumit+peter.attendees=ale@x.com&attendees=rumit@x.comrumit-only filter — last param wins)attendees=rumit@x.com&attendees=ale@x.com(same values, order swapped)ale-only filter — last param wins, not order-independent)attendees=ale@x.com,rumit@x.com(comma-joined, single param)So passing the SDK's
attendeeslist 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 forinFolderinListThreadsQueryParams), it'd be worth adding a small piece of shared, declarative metadata toIQueryParamsso every affected field opts in with one line, right next to its existing "comma-delimited" docstring: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 touchaddQueryParamsat all, and replaces the existinginFolder-only special case inListThreadsQueryParamswith the same general mechanism. Any other field documented as a scalar comma-delimited string (to/cc/bcc/any_emailonListMessagesQueryParams/ListThreadsQueryParams) could opt in the same way.Note:
event_typeonListEventQueryParamsshould NOT be added tocommaDelimitedKeys— its docs explicitly document repeated-param OR semantics with a worked example (event_type=default&event_type=outOfOffice→ returns events that aredefaultORoutOfOffice), so the current repeated-key serialization viaaddQueryParams's genericis 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 forinFolder) so callers aren't surprised.