Skip to content

Hoisting

Hoisting is a technique used in API design to reorganize the structure of data in API requests and responses. Its main goal is to simplify the way data is presented by moving nested or deeply structured data to a higher level in the API’s response or request body. This process makes the data easier to work with for developers by reducing complexity and aligning the structure more closely with how resources are conceptually understood.

In essence, hoisting “flattens” data structures. For APIs, this means transforming responses or requests so that important information is more accessible and not buried within nested objects. This is particularly beneficial when dealing with APIs that serve complex data models, as it can make the data easier to parse and use without extensive traversal of nested objects.

Hoisting is usually applied in specific scenarios to improve the design and usability of APIs:

  • Complex Nested Structures: Employ hoisting when your API deals with complex, deeply nested data. It streamlines access to important information, reducing the need for deep navigation.
  • Frequent Data Access: Use hoisting for elements that are often accessed or critical to operations, making them more directly accessible.
  • Data Model Alignment: Apply hoisting to better align the API’s data structure with the conceptual model of the resources.

Initially, our data structure represents a hierarchical model with nested entities, depicted as a tree.

x-speakeasy-entity: 1 at the top, with entities 2 and 3 as direct descendants. Entity 2 further nests entities 4, which branches into 5 and 6.

Terminal window
(1)
/ \
2 3
\
4
/ \
5 6

Entity (2) is marked with x-speakeasy-entity for hoisting.

Terminal window
1
/ \
(2) 3
\
4
/ \
5 6

After applying hoisting, the structure is reorganized to prioritize x-speakeasy-entity: 2, making its leaf nodes directly accessible and flattening the remaining structure.

x-speakeasy-entity: 2

Terminal window
x-speakeasy-entity: 2
(2)
/ \
3 4
/ \
5 6

Real-World Application: Flattening a “data” property

Section titled “Real-World Application: Flattening a “data” property”

The JSON Schemas for Drink, Drink, and { drinkType: $DrinkType } will be each considered the root of a Terraform Type Schema, and will be merged together to form the final Terraform Type Schema using Attribute Inference.

However, this is not always desired. Consider this alternative response:

In the original API design, the request and response body are structured equivalently, without nested elements. This approach is straightforward but might not always suit complex data relationships or requirements.

/drinks:
post:
x-speakeasy-entity-operation: Drink#create
parameters:
- name: type
in: query
description: The type of drink
required: false
deprecated: true
schema:
$ref: "#/components/schemas/DrinkType"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Drink"
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Drink"
"5XX":
$ref: "#/components/responses/APIError"
default:
$ref: "#/components/responses/UnknownError"

The alternate approach introduces a nested data property in the API response, which can encapsulate the drink information more distinctly, albeit adding a layer of complexity in data access.

/drinks:
post:
x-speakeasy-entity-operation: Drink#create
parameters:
- name: type
in: query
description: The type of drink
required: false
deprecated: true
schema:
$ref: "#/components/schemas/DrinkType"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Drink"
responses:
"200":
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Drink"
"5XX":
$ref: "#/components/responses/APIError"
default:
$ref: "#/components/responses/UnknownError"

Using the same request/response bodies, speakeasy would generate the following type schema

func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Drink Resource",
Attributes: map[string]schema.Attribute{
"drink_type": schema.StringAttribute{
Optional: true,
Computed: true
Description: `The type of drink.`,
stringvalidator.OneOf(
"cocktail",
"non-alcoholic",
"beer",
"wine",
"spirit",
"other",
),
},
"name": schema.Int64Attribute{
Required: true,
Description: `The name of the drink.`,
},
"price": schema.StringAttribute{
Required: true,
Description: `The price of one unit of the drink in US cents.`,
},
},
}
}

When generated, the provider schema would look like this: not understanding that data was a nested object, and instead treating it as a root of the schema.

func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Drink Resource",
Attributes: map[string]schema.Attribute{
"drink_type": schema.StringAttribute{
Optional: true,
Description: `The type of drink.`,
stringvalidator.OneOf(
"cocktail",
"non-alcoholic",
"beer",
"wine",
"spirit",
"other",
),
},
"name": schema.Int64Attribute{
Required: true,
Description: `The name of the drink.`,
},
"price": schema.StringAttribute{
Required: true,
Description: `The price of one unit of the drink in US cents.`,
},
"data": schema.SingleNestedAttribute{
Computed: true,
Attributes: map[string]schema.Attribute{
"drink_type": schema.StringAttribute{
Computed: true
Description: `The type of drink.`,
},
"name": schema.Int64Attribute{
Computed: true
Description: `The name of the drink.`,
},
"price": schema.StringAttribute{
Computed: true,
Description: `The price of one unit of the drink in US cents.`,
},
},
},
},
}
}

By applying the x-speakeasy-entity annotation, we direct the schema generation process to consider the Drink schema as the root of the type schema, effectively flattening the response structure for easier access.

/drinks:
post:
x-speakeasy-entity-operation: Drink#create
parameters:
- name: type
in: query
description: The type of drink
required: false
deprecated: true
schema:
$ref: "#/components/schemas/DrinkType"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Drink"
responses:
"200":
content:
application/json:
schema:
type: object
properties:
data:
$ref: "#/components/schemas/Drink"
"5XX":
$ref: "#/components/responses/APIError"
default:
$ref: "#/components/responses/UnknownError"
components:
schemas:
Drink:
x-speakeasy-entity: Drink
type: object
properties:
name:
description: The name of the drink.
type: string
examples:
- Old Fashioned
- Manhattan
- Negroni
type:
$ref: "#/components/schemas/DrinkType"
price:
description: The price of one unit of the drink in US cents.
type: number
examples:
- 1000 # $10.00
- 1200 # $12.00
- 1500 # $15.00
required:
- name
- price

The final provider schema reflects a flattened structure, similar to the original API design but with the flexibility to include nested data when necessary.

func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Drink Resource",
Attributes: map[string]schema.Attribute{
"drink_type": schema.StringAttribute{
Optional: true,
Computed: true
Description: `The type of drink.`,
stringvalidator.OneOf(
"cocktail",
"non-alcoholic",
"beer",
"wine",
"spirit",
"other",
),
},
"name": schema.Int64Attribute{
Required: true,
Description: `The name of the drink.`,
},
"price": schema.StringAttribute{
Required: true,
Description: `The price of one unit of the drink in US cents.`,
},
},
}
}