Skip to content

Basic methods

This page documents the default generation logic for client’s basic method as well as how to customize it. For advanced paging, long-running or multipart operations, see the Paging Operations, Long-Running Operations and Multipart Operations pages.

By default, any language code generator will generate both protocol methods and convenient methods for an operation.

NOTE: Python and Typescript don’t have a separation of convenient/protocol methods.

main.tsp
namespace PetStoreNamespace;
/** This is the input I need */
@resource("output")
model OutputModel {
/** Id of this object */
@key
@visibility(Lifecycle.Read)
name: string;
}
/** Read my resource */
op GetModel is ResourceRead<OutputModel>;
class OutputModel:
name: str = rest_field(readonly=True)
response: OutputModel = client.get(name="name")

The detailed generation configuration of protocol and/or convenient methods that can be done:

As emitters global parameters:

  • generate-protocol-methods: boolean flag to shift the entire generation for the process (true by default)
  • generate-convenience-methods: boolean flag to shift the entire generation for the process (true by default)

To set global emitters parameters, read the documentation of emitters configuration.

For fine tuning, the set of decorators @protocolAPI and @convenientAPI can be used. They take a required boolean as parameter.

Shifting the generation of protocol and convenience on and off

Section titled “Shifting the generation of protocol and convenience on and off”

This can be achieved with the augment operator and the emitter package

client.tsp
import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";
using Azure.ClientGenerator.Core;
@@convenientAPI(PetStoreNamespace.GetModel, false);
# Python do not change behavior based on protocolAPI or convenientAPI

Sometimes it may be useful to still generate the method, but to make it private, so it can be re-used by a manual code wrapper.

The two possible value for the Access enum are internal and public.

client.tsp
import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";
using Azure.ClientGenerator.Core;
@@access(PetStoreNamespace.GetModel, "internal");
# can't import form models directly
from petstorenamespace.models import GetModel # will report error

Models can be used for input, output, or both at the same time. In some languages, this changes the way the API is exposed for those models.

By default, the code generator will infer the usage based on the TypeSpec. If this inference doesn’t correspond to expectation, this can be customized with the usage decorator. Possible values are input and output, and can be combined with Usage.input | Usage.output.

NOTE: If a model is never used, it will not be generated. Assigning a usage will force generation.

client.tsp
import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";
using Azure.ClientGenerator.Core;
// This model is input only
@@usage(Azure.OpenAI.AzureCognitiveSearchIndexFieldMappingOptions, Usage.input);
// This models is input/output
@@usage(Azure.OpenAI.ImageGenerations, Usage.input | Usage.output);
# Python doesn't generate different code based on usage
# However, the model may not be generated if it's never used
# In that case, set a usage for the model
model User {
firstName: string;
lastName: string;
}
@get
op get(): User;
@post
op post(@body body: User): void;
def get() -> User:
...
@overload
def post(body: User, **kwargs: Any) -> None:
...
@overload
def post(body: JSON, **kwargs: Any) -> None:
...
@overload
def post(body: IO[bytes], **kwargs: Any) -> None:
...
def post(body: [User, JSON, IO[bytes]], **kwargs: Any) -> None:
...

Please exercise caution when using the spread feature.

  • The model to be spread should have fewer than 6 settable properties. See simple methods.
  • The model to be spread should remain stable across api-versions. Adding an optional property across api-versions could result in one additional method overload in SDK client.
  • The model to be spread should not be used in JSON Merge Patch.
alias User = {
firstName: string;
lastName: string;
};
@post
op upload(...User): void;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

def upload(first_name: str, last_name: str) -> None:
...
alias User = {
@path id: string;
firstName: string;
lastName: string;
};
op upload(...User): void;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

def upload(self, id: str, first_name: str, last_name: str, *, content_type: str = "application/json") -> None:
...
model User {
firstName: string;
lastName: string;
}
op upload(...User): void;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

def upload(self, first_name: str, last_name: str, *, content_type: str = "application/json") -> None:
...
model User {
firstName: string;
lastName: string;
}
model UserRequest {
@body user: User;
}
op upload(...UserRequest): void;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

def upload(self, body: [User, JSON, IO[bytes]], *, content_type: str = "application/json") -> None:
...
model BlobProperties {
@path
name: string;
@header
testHeader: string;
}
@route("blob_properties/{name}")
op getBlobProperties(...BlobProperties): void;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

def get_blob_properties(self, name: str, *, test_header: string, content_type: str = "application/json") -> None:
...

Model mixed with normal and @header/@query/@path properties

Section titled “Model mixed with normal and @header/@query/@path properties”
model Schema {
@header contentType: "application/json";
schema: bytes;
}
@post
op register(...Schema): void;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

class Schema:
schema: bytes
def register(self, body: [Schema, JSON, IO[bytes]], *, content_type: str = "application/json") -> None:
...

Resource create and update operations are not impacted by spread since they all have explicit defined body parameter. Only resource action operations are impacted by spread.

If the action parameter is a model, then the model will be spread.

@resource("widgets")
model Widget {
@key("widgetName")
name: string;
}
model RepairInfo {
problem: string;
contact: string;
}
model RepairResult {
reason: string;
info: string;
}
alias Operations = Azure.Core.ResourceOperations<{}>;
op scheduleRepairs is Operations.ResourceAction<Widget, RepairInfo, RepairResult>;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

class RepairInfo:
problem: str
contact: str
class RepairResult:
reason: str
info: str
def scheduleRepairs(self, widget_name: str, problem: str, contact: str, *, content_type: str = "application/json") -> RepairResult:
...

If you want to keep the model, you have two options to prevent spreading:

  • @override
  • If you don’t want to do client customizations, you could use a wrapper to explicitly set the body to prevent spread.
  1. @override

The @override decorator allows you to replace the default client method generated by TCGC based on your service definition.

If your service definition spreads a model into the method signature, but you prefer that the generated client SDKs maintain the model intact, the @override decorator provides a solution.

Additionally, you can specify a language scope to limit the changes to a specific language emitter.

client.tsp
namespace Widget.Client;
op scheduleRepairs(
body: RepairInfo,
`api-version`: Azure.Core.Foundations.ApiVersionParameter,
): RepairResult;
@@override(Widget.Service.scheduleRepairs, Widget.Client.scheduleRepairs);
  1. wrapper

If you prefer not to implement customizations in client.tsp, you can add @bodyRoot to the input in main.tsp prior to passing the model to the template.

main.tsp
namespace Widget.Service;
@resource("widgets")
model Widget {
@key("widgetName")
name: string;
}
model RepairInfo {
problem: string;
contact: string;
}
model RepairResult {
reason: string;
info: string;
}
alias Operations = Azure.Core.ResourceOperations<{}>;
alias BodyParameter<
T,
TName extends valueof string = "body",
TDoc extends valueof string = "Body parameter."
> = {
@doc(TDoc)
@friendlyName(TName)
@bodyRoot
body: T;
};
op scheduleRepairs is Operations.ResourceAction<Widget, BodyParameter<RepairInfo>, RepairResult>;

For Python, we will also generate the overloads described in the HTTP Post section, but will omit them for brevity.

class RepairInfo:
problem: str
contact: str
class RepairResult:
reason: str
info: str
def scheduleRepairs(self, body: [Schema, JSON, IO[bytes]], *, content_type: str = "application/json") -> RepairResult:
...

The following sections documents emitter behavior and customization when you use union operator | or @sharedRoute to express multiple input/output for a given path.

The simplest way to express a combination of input in TypeSpec is to use the union operator |. At a glance, JS and Python supports natively union, while Java and C# will use overloads.

client.tsp
@service(#{ title: "Analyze", version: "v1" })
namespace Analyze;
@route("/analyze")
@post
op analyze(@query mode: "strict" | "lenient", @body image: bytes): AnalyzeResult;
model CompletionInput {
input: string | string[];
}
@route("/completions")
@post
op completions(@body input: CompletionInput): CompletionResult;
def analyze(image: bytes, *, mode: Literal["strict", "lenient"]) -> AnalyzeResult:
...
class CompletionInput:
input: Union[str, List[str]] = rest_field(readonly=True)
def completions(input: CompletionInput) -> CompletionResult:
...

Using union implies that the entire combination of possible input is valid. If you have a specific set of combination, or connection between input and output, you must use @sharedRoute. By default, codegen will generate one method per operation name.

client.tsp
@sharedRoute
@route("/foo")
op a(x: int32): float;
@sharedRoute
@route("/foo")
op b(x: string): int64;
def a(x: int) -> float:
# code
def b(x: string) -> int:
# code

If your shared routes are actually one unique semantic operation, you may want to configure codegen to use a unique name. This is simply done by renaming both operations to the same name using @clientName

main.tsp
@sharedRoute
@route("/foo")
op a(x: int) : float
@sharedRoute
@route("/foo")
op b(x: string) : int64
// client.tsp
import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";
using Azure.ClientGenerator.Core;
@@clientName(a, "Foo");
@@clientName(b, "Foo");
@overload
def foo(x: int) -> float:
...
@overload
def foo(x: string) -> int:
...
def foo(x: string | int) -> float | int:
# Code here

The following sections document how to generate client libraries for conditional request headers such as If-Match and If-None-Match.

model Response {
name: string;
}
@get
op get(
/** The request should only proceed if an entity matches this string. */
@header("If-Match")
ifMatch?: string,
/** The request should only proceed if no entity matches this string. */
@header("If-None-Match")
ifNoneMatch?: string,
): Response;
from azure.core import MatchConditions
def get(self, *, etag: Optional[str] = None, match_condition: Optional[MatchConditions] = None) -> Response: