Multiclass APIs

If a single API is particularly complex, you may wish to implement it from multiple Java classes. To make different classes part of the same API, simply give each class the same name and version strings in their @Api annotation. For example, the following two classes will both be part of the tictactoe API:

@Api(name = "tictactoe", version = "v1")
class TicTacToeA { … }

@Api(name = "tictactoe", version = "v1")
class TicTacToeB { … }

The API configuration is specified through the @Api annotation properties. However, for multiple classes in the same API, the @Api requirements extend beyond simply having the same name and version strings in the @Api annotation for each class. In fact, your backend API will not work if there are any differences in the API configurations specified in the classes' @Api properties. Any difference in the @Api properties for classes in a multiclass API result in an "ambiguous" API configuration, which will not work in Endpoints.

There are several ways to create a unambiguous multiclass API:

  • Manually ensure that all classes in a single API have the exact same @Api annotation properties. We won't describe this further, since it is self-evident.
  • Use annotation inheritance through Java inheritance. In this inheritance, all classes in a single API inherit the same API configuration from a common @Api-annotated base class.
  • Use annotation inheritance through the @ApiReference annotation on all classes in a single API to have them reference the same API configuration from a common @Api-annotated class.

Using @ApiClass for Properties that Can Differ Between Classes

While all properties in the @Api annotation must match for all classes in an API, you can additionally use the @ApiClass annotation to provides properties that do not need to be exactly the same between classes. For example:

// API methods implemented in this class allow only "clientIdA".
@Api(name = "tictactoe", version = "v1")
@ApiClass(clientIds = { "clientIdA" })
class TicTacToeA { … }

// API methods implemented in this class provide unauthenticated access.
@Api(name = "tictactoe", version = "v1")
class TicTacToeB { … }

where TicTacToeA limits access using a whitelist of client IDs containing the allowed client ID, and TicTacToeB does not limit acccess.

All properties provided by the @ApiClass annotation have an equivalent property in the @Api annotation. Note that the @Api equivalent property acts as the API-wide default. If there is an API-wide default for that same property, specified in @Api, the class-specific @ApiClass property will override the API-wide default.

The following examples illustrate the overriding of @Api properties by the class specific @ApiClass equivalents:

// For this class "boards" overrides "games".
@Api(name = "tictactoe", version = "v1", resource = "games")
@ApiClass(resource = "boards")
class TicTacToeBoards { … }

// For this class "scores" overrides "games".
@Api(name = "tictactoe", version = "v1", resource = "games")
@ApiClass(resource = "scores")
class TicTacToeScores { … }

// For this class, the API-wide default "games" is used as the resource.
@Api(name = "tictactoe", version = "v1", resource = "games")
class TicTacToeGames { … }

Annotation Inheritance

The @Api and @ApiClass annotation properties can be inherited from from other classes, and individual properties can be overridden either through Java Inheritance or @ApiReference inheritance

Using Java Inheritance

A class that extends another class with @Api or @ApiClass annotations will behave as if annotated with the same properties. For example:

@Api(name = "tictactoe", version = "v1")
class TicTacToeBase { … }

// TicTacToeA and TicTacToeB both behave as if they have the same @Api annotation as
// TicTacToeBase
class TicTacToeA extends TicTacToeBase { … }
class TicTacToeB extends TicTacToeBase { … }

Annotations are only inherited through Java subclassing, not through interface implementation. For example:

@Api(name = "tictactoe", version = "v1")
interface TicTacToeBase { … }
// Does *not* behave as if annotated.
class TicTacToeA implements TicTacToeBase { … }

As a result, there is no support for any sort of multiple inheritance of Endpoints annotations.

Inheritance works for @ApiClass too:

@ApiClass(resource = "boards")
class BoardsBase { … }

// TicTacToeBoards behaves as if annotated with the @ApiClass from BoardsBase.
// Thus, the "resource" property will be "boards".
@Api(name = "tictactoe", version = "v1", resource = "scores")
class TicTacToeBoards extends BoardsBase { … }

where TicTacToeBoards inherits the resource property value boards from BoardsBase, thus overriding the resource property setting (scores) in its @Api annotation. Remember that if any class has specified the resource property in the @Api annotation, all of the classes need to specify that same setting in the @Api annotation; this inheritance technique lets you override that @Api property.

Using @ApiReference Inheritance

The @ApiReference annotation provides an alternate way to specify annotation inheritance. A class that uses @ApiReference to specify another class with @Api or @ApiClass annotations will behave as if annotated with the same properties. For example:

@Api(name = "tictactoe", version = "v1")
class TicTacToeBase { … }

// TicTacToeA behaves as if it has the same @Api annotation as TicTacToeBase
@ApiReference(TicTacToeBase.class)
class TicTacToeA { … }

If both Java inheritance and the @ApiReference are used, the annotations will inherit through the @ApiReference annotation only. @Api and @ApiClass annotations on the class inherited through Java inheritance will be ignored. For example:

@Api(name = "tictactoe", version = "v1")
class TicTacToeBaseA { … }
@Api(name = "tictactoe", version = "v2")
class TicTacToeBaseB { … }

// TicTacToe will behave as if annotated the same as TicTacToeBaseA, not TicTacToeBaseB.
// The value of the "version" property will be "v1".
@ApiReference(TicTacToeBaseA.class)
class TicTacToe extends TicTacToeBaseB { … }

Overriding Inherited Configuration

Whether inheriting configuration using Java inheritance or @ApiReference, the inherited configuration can be overridden using a new @Api or @ApiClass annotation. Only configuration properties specified in the new annotation are overridden. Properties that are unspecified are still inherited. For example:

@Api(name = "tictactoe", version = "v2")
class TicTacToe { … }

// Checkers will behave as if annotated with name = "checkers" and version = "v2"
@Api(name = "checkers")
class Checkers extends TicTacToe { … }

Overriding inheritance works for @ApiClass too:

@Api(name = "tictactoe", version = "v1")
@ApiClass(resource = "boards", clientIds = { "c1" })
class Boards { … }

// Scores will behave as if annotated with resource = "scores" and clientIds = { "c1" }
@ApiClass(resource = "scores")
class Scores { … }

Overriding also works when inheriting through @ApiReference:

@Api(name = "tictactoe", version = "v2")
class TicTacToe { … }

// Checkers will behave as if annotated with name = "checkers" and version = "v2"
@ApiReference(TicTacToe.class)
@Api(name = "checkers")
class Checkers { … }

Inheriting @ApiMethod Annotations

The @ApiMethod annotation can be inherited from overridden methods. For example:

class TicTacToeBase {
  @ApiMethod(httpMethod = "POST")
  public Game setGame(Game game) { … }
}
@Api(name = "tictactoe", version = "v1")
class TicTacToe extends TicTacToeBase {
  // setGame behaves as if annotated with the @ApiMethod from TicTacToeBase.setGame.
  // Thus the "httpMethod" property will be "POST".
  @Override
  public Game setGame(Game game) { … }
}

Similarly to @Api and @ApiClass annotation inheritance, if multiple methods overriding each other have @ApiMethod annotations, individual properties can be overridden. For example:

class TicTacToeBase {
  @ApiMethod(httpMethod = "POST", clientIds = { "c1" })
  public Game setGame(Game game) { … }
}
@Api(name = "tictactoe", version = "v1")
class TicTacToe extends TicTacToeBase {
  // setGame behaves as if annotated with httpMethod = "GET" and clientIds = { "c1"}.
  @ApiMethod(httpMethod = "GET")
  @Override
  public Game setGame(Game game) { … }
}

There is no @ApiReference annotation or equivalent for methods, so @ApiMethod is always inherited through Java inheritance, not through @ApiReference.

Inheritance and Precedence Rules

To synopsize the preceding discussion, the follow table shows the inheritance rules and order of precedence.

Annotation/inheritance Rule
@Api Must be identical for all classes.
@ApiClass Specified for a class to override @Api properties.
Java inheritance Class inherits @Api and @ApiClass of base class.
@ApiReference Class inherits @Api and @ApiClass of referenced class.
Using @ApiReference on a class (Java) inheriting from a base class Class inherits the @Api and @ApiClass of referenced class, not from the base class.

Common Use Cases for Annotation Inheritance

The following are examples of the typical use cases for inheritance:

For API versioning:

@Api(name = "tictactoe", version = "v1")
class TicTacToeV1 { … }
@Api(version = "v2")
class TicTacToeV2 extends TicTacToeV1 { … }

For multiclass APIs:

@Api(name = "tictactoe", version = "v1")
class TicTacToeBase {}
@ApiClass(resource = "boards")
class TicTacToeBoards extends TicTacToeBase { … }
@ApiClass(resource = "scores")
class TicTacToeScores extends TicTacToeBase { … }

For testing different versions of the same API:

@Api(name = "tictactoe", version = "v1")
class TicTacToe {
  protected Foo someMethod() {
    // Do something real;
  }

  public Foo getFoo() { … }
}

@Api(version="v1test")
class TicTacToeTest extends TicTacToe {
  protected Foo someMethod() {
    // Stub out real action;
  }
}

where someMethod might return pre-determined responses, avoid calls with side-effects, skip a network or datastore request, and so forth.