The Future of the Asana Client Libraries

Hello everyone!

We are working on upgrading the tooling of our client libraries. We have run into a decision point where we would like your input!

For typed languages, we’re discussing how the models should look and interact with request functions. The main issue is that it’s difficult to have a single Model represent both the request object & the response object, when they have different properties and/or un-returned properties.

Asana’s API has asymmetric requests & responses. Take the Task object for example:

POST /tasks

{
  name: “Example”,
  assignee: “1234567890”,
  workspace: “123457890"
}

GET /tasks/1234567890

{
  gid: “1234567890”,
  name: “Example”,
  assignee: {
    gid: “1234567890”,
    name: “Ross Grambo”
  },
  workspace: {
    gid: “1234567890”,
    name: “Main Workspace”
  }
}

For languages that are not using types, this is not an issue. However, when types are introduced, what is the type of assignee? Is it Option<String> or Option<User>?

Additionally, does assignee: null mean that the assignee field was not returned in the response or does it mean that the assignee is unassigned?

Our API allows for a lot of flexibility, which makes it hard to capture in a typed language.

Here’s some approaches we’ve discussed:

We have Request & Response models

  1. Developer experience takes a hit, as you’re now dealing with different types depending on the context.
  2. The response models would probably use something like Option<Option<User>> to represent
    assignee being unassigned vs not returned.

Requests & Responses use nested Dictionaries (or Gson)

  1. Developer experience takes a hit, as developers will need to convert to and from models. The model will only represent 1 of the forms (assignee probably being Option)
  2. Assuming developers are converting to a Model, they are expected to look into the Dictionary to see if assignee was returned or not, and optionally convert Dictionary.assignee to a User object

We choose 1 model, but do magic under the hood to have it work for requests and responses

  1. The developer experience might be unintuitive (sometimes an assignee is a user, sometimes it’s just an id, and you’re not in charge of it).
  2. This approach would require more maintenance and one-off handling for things like custom fields.
  3. We might still need the Option<Option<User>> format. We might be able to due without it the first Option<> layer, if we include the raw_response on models.

Does anyone have a preference? Or an idea? Or agree with one approach vs another? I currently prefer a single model, with a raw_response field, where we try and handle the asymmetry gracefully.

Note: we will most likely move to the new tooling and keep the libraries the same until we’ve decided on a consistent methodology & justified the breaking changes it makes

cc: @Phil_Seeman & @Bastien_Siebman

Thanks all,
Ross

4 Likes

I’d vote for We have Request & Response models

I’ve used other APIs where there are different models for requests and responses, so I don’t find it unusual and have never found it to be cumbersome. It feels to me like it follows the Single Responsibility Principle.

Additional thoughts:

Requests & Responses use nested Dictionaries (or Gson)
This would be my second choice, but I think it’s more complex to understand and implement than the separate-model approach.

We choose 1 model, but do magic under the hood to have it work for requests and responses
My least favorite (sorry, Ross!).
I really don’t like this:

The developer experience might be unintuitive (sometimes an assignee is a user, sometimes it’s just an id, and you’re not in charge of it).

This approach feels to me like it violates the SRP.

6 Likes

Hi,

I agree with @Phil_Seeman and I prefer when API sticks to the same rules everywhere.
It’s much easier for dev to work with API from IDE when you can see what actually is returned. Like this, you even don’t need to go to help center to know how to get the field you need.

So, if User class is going to have only 1 field gid it sounds absolutely good for me.

What if you decide to add another field in 1 year? In case of plain string you will have to change this in multiple places where this plain string is used and in case of a model you will just add a new field. The same is applicable for auto-docs, refactoring, syntax highlighting, auto-suggestions e.t.c.

Plain JSON might be the solution as well but then I don’t see much difference from using API without the library (you can make direct requests and get plain JSON without the need of installing additional dependencies).

Regarding the null and the value is not returned it would be great if you could just skip the fields that are not returned because they are not selected by opt_fields. I was quite disappointed when I started using API and I had null values returned for the fields. I thought that something’s wrong as far as values are null. After some time of experimenting it turned out that you just need to ask for additional fields and they will get theirs values. It’s not very dev friendly.

Thank you :wink:

2 Likes

Good point, I agree with this suggestion - it can be confusing.

1 Like

Same here, so everything is very clear, nothing done under the hood.

1 Like

Sorry, late to the discussion, only just catching up on a huge email backlog :slight_smile:

For Ditto I’ve implemented a client library in Go.

The request/response models are separate, but both embed a common ‘Base’ class containing all the mutable fields so that the bulk of the record can be easily copied across from response to request. The create-only fields are then added in the ‘CreateTaskRequest’ for example.

TaskBase:
https://bitbucket.org/mikehouston/asana-go/src/b6d82853f3a7ffc26722fb7412f600acf9d6901c/tasks.go#lines-87

Task: (response)
https://bitbucket.org/mikehouston/asana-go/src/b6d82853f3a7ffc26722fb7412f600acf9d6901c/tasks.go#lines-197

CreateTaskRequest:
https://bitbucket.org/mikehouston/asana-go/src/b6d82853f3a7ffc26722fb7412f600acf9d6901c/tasks.go#lines-156

UpdateTaskRequest:
https://bitbucket.org/mikehouston/asana-go/src/b6d82853f3a7ffc26722fb7412f600acf9d6901c/tasks.go#lines-175

I admit Go is a little different than Java, etc. due to the lack of class-based inheritance. For Java I feel it probably makes sense to keep separate Request/Response types.

3 Likes

Any news on the subject @Ross_Grambo?

Yes and no.

We redefined our openapi spec to use statically typed schemas. You can check it out at https://raw.githubusercontent.com/Asana/developer-docs/master/defs/asana_oas.yaml

These spit out working models for our client libraries, but we have not placed them into the libraries yet, as they tend to either be a deprecation or a complicated optional addition. Our plan for non-static languages is to make them optional.

The current plan is to have request and response models, in the hierarchy defined in the spec. The model’s fields will have getters that read from an underlying hashset which defines if the item was implicitly set.

The reasoning for this is because something the request for:

TaskRequest task = new TaskRequest();
task.id = "1234567890";
task.name = "New name";
Task.updateTask(task);

needs to send something different than:

TaskRequest task = new TaskRequest();
task.id = "1234567890";
task.name = "New name";
task.assignee = null;
Task.updateTask(task);

Because java (and some other languages) instantiates objects with null by default, we need a way to specify if a field was implicitly set to null. This is also true for response objects coming from Asana, as task.assignee being null means two different things depending on your opt_fields. The hashset is populated by setters.

Another part is conversions from response objects to request objects. I believe this could be a part of V2, but is a blocker for releasing models. Let me know if y’all disagree.

Basically we haven’t had the bandwidth to offer these models in the client libraries yet, as they won’t work out of the box until we implement the hashmap logic. Otherwise I know someone will accidently null out their data in Asana, or be unable to send null.

We’re still in the phase where we can pivot. Let me know if any of this sounds unreasonable or you have better suggestions on how this can be done.

If you want models ASAP, you can use our openapi spec with Swagger to generate your own. They work great, until you try and send data to Asana and have to deal with what null really means. In languages that offer undefined, assuming your request library doesn’t throw up, they will work out of the box.

Currently, the models for java and ruby are manually maintained.

1 Like

Thanks. I agree that this should be an opt-in, it is too complicated/hard/dangerous to have to go through all our existing code to check if we are not null-ifying a field by mistake…

2 Likes