Skip to main content

Introducing Support for oneOf and anyOf in PHP SDKs

· 7 min read

We are introducing the special oneOf and anyOf types in our PHP SDKs.

Details

In OpenAPI specification, an API call can return multiple types of responses, or take in multiple types of requests:

paths:
/connection:
post:
requestBody:
content:
application/json:
schema:
anyOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
responses:
'200':
description: Updated,
content:
application/json":
schema:
anyOf:
- $ref: '#/components/schemas/Contact'
- $ref: '#/components/schemas/Employee'
...

The above OAS API call description shows a post API call that takes in either Cat or Dog as request and returns Contact or Employee as response, which in APIMatic's PHP SDK should look like:

$catOrDog = CatBuilder::init()
->name("kitty")
->weight(15);

$contactOrEmployee = $client->getConnectionController()->postConnection($catOrDog);

In the above API call code sample, $catOrDog can be anyOf Cat or Dog instance. Previously, it was sent as an associative array instead of proper model instances.

Let's look at the response type of the same API call in the following example:

$contactOrEmployee = $client->getConnectionController()->postConnection($catOrDog);

Assert::assertInstanceOf(Contact::class, $contactOrEmployee); // true
Assert::assertInstanceOf(Employee::class, $contactOrEmployee); // false

In the above example, the API call postConnection() could return anyOf Contact or Employee instance in its response. But previously, it was only returning an associative array.

Now, with the introduction of oneOf and anyOf support, we can assert that the response can be of either types based on the received JSON data.

What has Changed?

Our PHP SDKs now have the ability to handle multiple types in requests and responses. This support for oneOf and anyOf also brings in type validations in our PHP SDKs.

Introducing @MapsBy annotation

We are introducing the special oneOf and anyOf type group string formats for our PHP SDKs, which will be used via annotation @mapsBy.

To have a deeper understanding of this, consider A and B are any two types, then:

  • oneOf(A,B) represents either A or B not both.
  • anyOf(A,B) represents either A, B or both.
  • oneOf(A,B)[] represents an array where an element can either be A or B not both.
  • anyOf(A,B)[] represents an array where an element can either be A, B or both.
  • array<string,oneOf(A,B)> represents a map where an element can either be A or B not both.
  • array<string,anyOf(A,B)> represents a map where an element can either be A, B or both.
  • oneOf{discField}(A{discValA},B{discValB}) represents the oneOf between A and B while the discField represents the discriminator property while discValA and discValB represents the discriminator values for their respective models.

In all the above formats, A or B might represent a simple type like string, int, Model, float[] or they may also represent a combination of types like, anyOf string and int i.e. anyOf(string,int), oneOf Car and map of float i.e. oneOf(Car,array<string,float>) or an array of oneOf Person and Employee i.e. oneOf(Person,Employee)[].

Changes in Models

Introducing union type hints over setters for combination of multiple types along with a newly introduced annotation @mapsBy which shows the actual combination of types in a special string format that we have already discussed above. The following example shows a setter with union type hint Morning|Evening while @mapsBy annotation shows the type of combination anyOf.

/**
* Sets Connection.
*
* @required
* @maps connection
* @mapsBy anyOf(Contact,Employee)
*
* @param Contact|Employee $connection
*/
public function setConnection($connection): void
{
$this->connection = $connection;
}

The @mapsBy annotation will be used during the deserialization of a JSON into a specific response model.

Actual union types are only introduced for PHP 8.1 and above, but our SDKs must support PHP 7.2 runtime. Which is why we are currently using union type hints in setters and getters.

So, this actual mixed type opens door for our SDK users to still use our traditional setter to set any value other than the required one. To avoid such cases, we are now verifying types for all fields which are using multiple type groups before the model's serialization in jsonSerialize(), i.e.

public function jsonSerialize()
{
$json = [];
$json['connection'] = ApiHelper::getJsonHelper()->verifyTypes(
$this->connection,
'anyOf(Contact,Employee)'
);

return $json;
}

In the above example verifyTypes will verify whether the field connection can be mapped by a model Contact or Employee.

Changes in Endpoints

We have now started using union type hints for endpoint's requests and responses.

In requests, type verification is also being done through strictType method by passing the union type format with the parameters.

The following endpoint example shows oneOf/anyOf changes and type verification in the request parameter connection. Here connection’s union type hint is Contact|Employee.

/**
* @param Contact|Employee $connection
*
* @return ServerResponse Response from the API call
*
* @throws ApiException Thrown if API call fails
*/
public function sendConnection($connection): ServerResponse {
$_reqBuilder = $this->requestBuilder(RequestMethod::POST, '/send/connection')
->parameters(
FormParam::init('connection', $connection)
->strictType('anyOf(Contact,Employee)')
);

$_resHandler = $this->responseHandler()->type(ServerResponse::class);

return $this->execute($_reqBuilder, $_resHandler);
}

In responses, type verification will be done through typeGroup method by passing the union type format in the response handler.

The following endpoint example shows oneOf/anyOf changes in response, where its type is described by a union type hint Contact|Employee while the function call typeGroup is responsible for mapping response JSON data by the specified type group.

/**
* @return Contact|Employee Response from the API call
*
* @throws ApiException Thrown if API call fails
*/
public function getConnection() {
$_reqBuilder = $this->requestBuilder(RequestMethod::POST, '/get/connection');

$_resHandler = $this->responseHandler()->typeGroup('anyOf(Contact,Employee)');

return $this->execute($_reqBuilder, $_resHandler);
}

Validation Exceptions

We are now introducing OneOfValidationException and AnyOfValidationException which will be thrown whenever an input with invalid type is provided to an endpoint or invalid typed data is received via response from the server. This feature will enforce type strictness and validation of requests and responses within the SDK.

Following code example will end up in OneOfValidationException catch block if the response JSON data maps to more then one of the multiple types. If none of the types are mapped to the response JSON data, then AnyOfValidationException catch block will be executed.

try {
$result = $client->getConnectionController()->getConnection();
var_dump($result);
} catch (OneOfValidationException $e) {
// If more then one type matches
echo 'Caught OneOfValidationException: ', $e->getMessage(), "\n";
} catch (AnyOfValidationException $e) {
// If none of the types matches
echo 'Caught AnyOfValidationException: ', $e->getMessage(), "\n";
}

Readme Changes

Now, the PHP SDK README docs will also reflect the changes for oneOf and anyOf types in Parameters. It will also properly link all of the models to the model documentation.

readme after

Similarly, we have also covered the response types for oneOf and anyOf cases like:

readme response types after

Portal Changes

We have also added the capability to select and configure any of the types from our API Code Playground. With this feature, you can generate your own code samples for all the types.

Send Connection Portal