azure-sdk-for-cpp/eng/common/knowledge/customizing-client-tsp.md
Azure SDK Bot d5b73b7f6b
Sync eng/common directory with azure-sdk-tools for PR 12758 (#6822)
* add reference doc for adding client customizations in TypeSpec

* Update eng/common/knowledge/customizing-client-tsp.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Mariana Rios Flores <mariari@microsoft.com>

---------

Co-authored-by: Christopher Radek <Christopher.Radek@microsoft.com>
Co-authored-by: Christopher Radek <14189820+chrisradek@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mariana Rios Flores <mariari@microsoft.com>
2025-11-10 10:49:51 -08:00

12 KiB

TypeSpec Client Customizations Reference

Quick Setup

1. Project Structure

project/
├── main.tsp          # Your service definition
├── client.tsp        # Client customizations
└── tspconfig.yaml    # Compiler configuration

2. Basic client.tsp Template

import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";

using Azure.ClientGenerator.Core;

// Your customizations here

Client Customizations Namespace

client.tsp should have a file-level namespace if types (e.g. models, interfaces, operations, etc) are defined in client.tsp. Do not add a file-level namespace if one already exists. They are only required when types are defined in client.tsp.

import "./main.tsp";
import "@azure-tools/typespec-client-generator-core";

using Azure.ClientGenerator.Core;

namespace ClientCustomizations;

// Your customizations here

Universal Scope Parameter

IMPORTANT: All Azure.ClientGenerator.Core decorators support an optional scope parameter as their final parameter to target specific language emitters.

Scope Syntax

@decoratorName(/* decorator-specific params */, scope?: string)

Scope Patterns

Target Specific Languages

// Single language
@@clientName(Foo, "Bar", "python")
@@access(Foo.get, Access.internal, "csharp")

// Multiple languages (comma-separated)
@@clientName(Foo, "Bar", "python, javascript")

Exclude Languages (Negation)

// Exclude one language
@@clientName(Foo, "Bar", "!csharp") // All languages EXCEPT C#

// Exclude multiple languages
@@clientName(Foo, "Bar", "!python, !go") // All languages EXCEPT python and go

Language Identifiers

  • "csharp" - C#/.NET
  • "python" - Python
  • "java" - Java
  • "javascript" - TypeScript/JavaScript
  • "go" - Go
  • "rust" - Rust

Scope Best Practices

DO: Use negation for single language exclusions

// Good: Exclude only C#
@@clientName(get, "getFoo", "!csharp")

// Bad: List all other languages
@@clientName(get, "getFoo", "python, java, javascript, go")

DO: Combine scopes when logic is identical

// Good: Same customization for multiple languages
@@clientName(name, "sharedName", "python, go")

// Avoid: Duplicate decorators
@@clientName(name, "sharedName", "python")
@@clientName(name, "sharedName", "go")

DON'T: Overuse scopes without clear need

// Bad: Unnecessary scope for universal customization
@@clientName(MyService, "MyClient", "csharp, python, java, javascript, go")

// Good: No scope means all languages
@@clientName(MyService, "MyClient")

Core Decorators

@access

Purpose: Control visibility of types and operations in generated clients. Syntax: @access(value: Access.public | Access.internal, scope?: string) Usage:

// Hide internal operations
@@access(getFoo, Access.internal)

// Make models referenced by internal operations public
@@access(Foo, Access.public)

// Language-specific access
@@access(getFoo, Access.internal, "csharp")

Propagation Rules:

  • Operations marked Access.internal make their models internal
  • Operations marked Access.public make their models public
  • Namespace access propagates to contained types
  • Model access propagates to properties and inheritance hierarchy

@client

Purpose: Define root clients in the SDK. Restrictions: Cannot be used with @clientLocation decorator. Cannot be used as an augmentation (@@) decorator. Important: @client has to be used on a type defined in client.tsp, so a file-level namespace (e.g. namespace ClientCustomizations;) should be added if one does not exist. Syntax: @client(options: ClientOptions, scope?: string) Usage:

// Basic client
@client({ service: MyService })
interface MyClient {}

// Named client
@client({ service: MyService, name: "CustomClient" })
interface MyClient {}

// Split operations into multiple clients
@client({ service: PetStore, name: "FoodClient" })
interface FoodClient {
  feed is PetStore.feed;
}

@client({ service: PetStore, name: "PetClient" })
interface PetClient {
  pet is PetStore.pet;
}

@operationGroup

Purpose: Define sub-clients (operation groups). Restrictions: Cannot be used with @clientLocation decorator. Cannot be used as an augmentation (@@) decorator. Important: @operationGroup has to be used on a type defined in client.tsp, so a file-level namespace (e.g. namespace ClientCustomizations;) should be added if one does not exist. Syntax: @operationGroup(scope?: string) Usage:

@client({ service: MyService })
namespace MyClient;

@operationGroup
interface Pets {
  list is MyService.listPets;
  get is MyService.getPet;
}

@operationGroup
interface Users {
  list is MyService.listUsers;
  create is MyService.createUser;
}

@clientLocation

Purpose: Move operations between clients without restructuring. Restrictions: Cannot be used with @client or @operationGroup decorators. Syntax: @clientLocation(target: Interface | Namespace | string, scope?: string) Usage:

// Move to existing client
@@clientLocation(MyService.upload, AdminOperations);

// Move to new client
@@clientLocation(MyService.archive, "ArchiveClient");

// Move to root client
@@clientLocation(MyService.SubClient.health, MyService);

// Move parameter to client initialization
@@clientLocation(MyService.upload.subscriptionId, MyService);

@clientName

Purpose: Override generated names for SDK elements. Takes precedence over all other naming mechanisms. Important: Always use PascalCase or camelCase for the rename parameter to make it easier to combine language scopes. SDKs will apply language-specific naming conventions automatically. Syntax: @clientName(rename: string, scope?: string) Usage:

// Rename Type
@@clientName(PetStore, "PetStoreClient");

// Language-specific names
@@clientName(foo, "pythonicFoo", "python")
@@clientName(foo, "CSharpFoo", "csharp")

@clientNamespace

Purpose: Change the namespace/package of generated types in the client SDK. Syntax: @clientNamespace(rename: string, scope?: string) Usage:

// Change client namespace
@@clientNamespace(MyService, "MyClient");

// Move model to different namespace
@@clientNamespace(MyService.MyModel, "MyClient.Models")

@clientInitialization

Purpose: Add custom parameters to client initialization. Important: When @clientInitialization references a model defined in client.tsp, add a file-level namespace (e.g. namespace ClientCustomizations;) if one does not exist. Syntax: @clientInitialization(options: ClientInitializationOptions, scope?: string) Usage:

// Add initialization parameters
model MyClientOptions {
  connectionString: string;
}

@@clientInitialization(MyService, { parameters: MyClientOptions });

// With parameter aliasing
model MyClientOptions {
  @paramAlias("subscriptionId")
  subId: string;
}

@@clientInitialization(MyService, { parameters: MyClientOptions });

@alternateType

Purpose: Replace types in generated clients. Syntax: @alternateType(alternate: Type | ExternalType, scope?: string) Usage:

// Change property type
@@alternateType(Foo.date, string);

// Language-specific alternates
@@alternateType(Foo.date, string, "python")

// External type replacement
@@alternateType(uri, {
  identity: "System.Uri",
  package: "System",
}, "csharp")

@override

Purpose: Customize method signatures in generated clients. Restrictions: Only operation parameter signatures can be customized. Important: When @override references an operation defined in client.tsp, a file-level namespace (e.g. namespace ClientCustomizations;) should be added if one does not exist. Syntax: @override(override: Operation, scope?: string)

Usage:

// main.tsp
// Original operation
op myOperation(foo: string, bar: string): void;

// client.tsp
// Custom signature - combine into options
model MyOperationOptions {
  foo: string;
  bar: string;
}

op myOperationCustom(options: MyOperationOptions): void;

@@override(myOperation, myOperationCustom);

@scope

Purpose: Include/exclude operations from specific languages. Usage:

@@scope(Foo.create, "!csharp")      // All languages except C#

@@scope(Foo.create, "python")       // Python only

@@scope(Foo.create, "java, go")     // Java and Go only

@usage

Purpose: Add usage information to models and enums. Note: The usages provided are additive. Usage:

// Add input and output usage to type
@@usage(MyModel, Usage.input | Usage.output)

Usage Values:

  • Usage.input: Used in request
  • Usage.output: Used in response
  • Usage.json: Used with JSON content type
  • Usage.xml: Used with XML content type

@clientDoc

Purpose: Override documentation for client libraries. Usage:

// Replace type documentation with client documentation
@@clientDoc(myOperation, "Client-specific documentation", DocumentationMode.replace)

// Append type documentation with client documentation - for only python
@@clientDoc(myModel, "Additional client notes", DocumentationMode.append, "python")

Language-specific Customizations

@useSystemTextJsonConverter (C# only)

Purpose: Use custom JSON converter for backward compatibility. Usage:

@@useSystemTextJsonConverter(MyModel, "csharp")

Best Practices

Do's

  • Use client.tsp for all customizations
  • Use scope parameter for language-specific customizations
  • Prefer scope negation ("!csharp") when excluding single languages
  • Combine scopes ("python, java") when logic is identical across languages
  • Use a file-level namespace (e.g. namespace ClientCustomizations;) in client.tsp if any types are defined in client.tsp.

Don'ts

  • Mix @clientLocation with @client/@operationGroup
  • Over-customize - prefer defaults when possible
  • Use legacy decorators for new services
  • Rename without considering the impact on breaking changes
  • Forget to specify scope for language-specific customizations

Common Scenarios

Scenario 1: Move Operations to Root Client

// Before: Operations in interfaces become sub-clients
interface Pets { feed(): void; }
interface Users { login(): void; }

// After: All operations on root client
@@clientLocation(Pets.feed, MyService);
@@clientLocation(Users.login, MyService);

Scenario 2: Rename for Consistency

// Standardize naming across languages
@@clientName(MyService, "MyServiceClient");
@@clientName(getUserInfo, "GetUserInformation");

// Language-specific naming
@@clientName(uploadFile, "UploadFile", "csharp, python");

Scenario 3: Add Client Parameters

// Elevate common parameters to client
model MyServiceOptions {
  subscriptionId: string;
  resourceGroup: string;
}

@@clientInitialization(MyService, { parameters: MyServiceOptions });

Scenario 4: Multi-Client Architecture

// Separate admin and user operations
@client({ service: MyService, name: "AdminClient" })
interface AdminClient {
  deleteUser is MyService.deleteUser;
  manageRoles is MyService.manageRoles;
}

@client({ service: MyService, name: "UserClient" })
interface UserClient {
  getProfile is MyService.getProfile;
  updateProfile is MyService.updateProfile;
}

Scenario 5: Language-specific clients

// Different client names for Java and others
@client({ service: MyService, name: "Foo.MyServiceClient" }, "java")
@client({ service: MyService, name: "MyServiceClient" }, "!java")
interface MyServiceClient {
  getAllData is MyService.getAllData;
}

// Different clients for python and Go
@client({ service: MyService, name: "MyClient" }, "python")
interface MyClientPython {
  fetchData is MyService.fetchData;
}
@client({ service: MyService, name: "MyClient" }, "go")
interface MyClientGo {
  fetchStream is MyService.fetchStream;
}

Scenario 6: Rename custom client operations

// Create a client with operation names changed
@client({ service: MyService, name: "MyClient" })
interface MyClient {
  getFoo is MyService.getFooData;
}

This reference provides the essential patterns and decorators for TypeSpec client customizations. Focus on the core decorators (@client, @operationGroup, @@clientLocation, @@clientName, @@access) for most scenarios, and use advanced features selectively.