Legacy Hierarchy Building
This document explains how to use the @Azure.ClientGenerator.Core.Legacy.hierarchyBuilding decorator to change the base type of a model in generated client SDKs.
Overview
Section titled “Overview”The @hierarchyBuilding decorator changes the base type (parent class) of a model in the generated SDK type graph. It does not validate property presence at decoration time. Instead, property reconciliation happens during SDK type graph building.
Use @hierarchyBuilding when you need to:
- Rebase a model onto a different ancestor in the inheritance chain (e.g., skip intermediate models)
- Rebase a model onto an unrelated base that shares common properties (e.g., brownfield ARM resources onto
TrackedResource) - Create multi-level discriminated hierarchies in generated SDKs
Property Reconciliation Rules
Section titled “Property Reconciliation Rules”When the decorator is applied, the following rules govern how properties are reconciled:
1. Property Lifting
Section titled “1. Property Lifting”Properties from removed intermediate ancestors (between the target’s original parent and the new base) are “lifted” onto the rebased model as its own properties.
model C { c?: string;}model B extends C { b?: string;}@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(C)model A extends B { a?: string;}// SDK result: A extends C. A.properties = { a, b }. C.properties = { c }.// 'b' was lifted from removed intermediate B.class C: c: Optional[str]
class B(C): c: Optional[str] b: Optional[str]
class A(B): c: Optional[str] b: Optional[str] a: Optional[str]public partial class C{ public string CProperty { get; }}
public partial class B : C{ public string BProperty { get; }}
public partial class A : B{ public string AProperty { get; }}export interface C { c?: string;}
export interface B extends C { b?: string;}
export interface A extends B { a?: string;}public class C { private String c;}
public class B extends C { private String b; private String c;}
public final class A extends B { private String a; private String b; private String c;}type A struct { A *string B *string C *string}2. Duplicate Dropping
Section titled “2. Duplicate Dropping”Properties whose names are supplied by the new base chain are dropped from the model (they are inherited instead).
model B { propB: string;}model A { ...B; propA: string;}@@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(A, B);// SDK result: A extends B. A.properties = { propA }. B.properties = { propB }.// propB was A's own property via spread, but is now supplied by new base → dropped.class B: prop_b: str
class A(B): prop_b: str prop_a: strpublic partial class B{ public virtual string PropB { get; }}
public partial class A : B{ public override string PropB { get; } public string PropA { get; }}export interface B { propB: string;}
export interface A extends B { propB: string; propA: string;}public class B { private final String propB;}
public final class A extends B { private final String propB; private final String propA;}type A struct { // REQUIRED PropA *string
// REQUIRED PropB *string}
type B struct { // REQUIRED PropB *string}3. Type Compatibility Check
Section titled “3. Type Compatibility Check”When dropping a property, if the type on the dropped property is incompatible with the same-named property on the new base chain, a legacy-hierarchy-building-conflict warning with property-type-mismatch message is emitted. The property is still dropped.
model C { shared?: int32;}model OldBase { shared?: string; // different type than C.shared}@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(C)model A extends OldBase { a?: string;}// SDK result: A extends C. A.properties = { a }.// Warning emitted: 'shared' type mismatch (string vs int32).# A `legacy-hierarchy-building-conflict` warning is emitted at compile time.class OldBase: shared: Optional[str]
class A(OldBase): shared: Optional[str] a: Optional[str]// Warning: legacy-hierarchy-building-conflict (property-type-mismatch for 'shared').public partial class OldBase{ public string Shared { get; }}
public partial class A : OldBase{ public string AProperty { get; }}// Warning: legacy-hierarchy-building-conflict (property-type-mismatch for "shared").export interface OldBase { shared?: string;}
export interface A extends OldBase { a?: string;}// Warning: legacy-hierarchy-building-conflict (property-type-mismatch for "shared").public class OldBase { private String shared;}
public final class A extends OldBase { private String a; private String shared;}// Warning: legacy-hierarchy-building-conflict (property-type-mismatch for "shared").type A struct { A *string Shared *string}Compatible types are silently dropped without warnings:
scalar azureLocation extends string;model C { kind: string; location: string;}model OldBase { kind: "old"; location: azureLocation;}@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(C)model A extends OldBase { a?: string;}// SDK result: A extends C. A.properties = { a }.// No warning: literal "old" is assignable to string, azureLocation is assignable to string.class OldBase: kind: Literal["old"] = "old" location: str
class A(OldBase): kind: Literal["old"] = "old" location: str a: Optional[str]public partial class OldBase{ public string Kind { get; } = "old"; public string Location { get; }}
public partial class A : OldBase{ public string AProperty { get; }}export interface OldBase { kind: "old"; location: string;}
export interface A extends OldBase { a?: string;}public class OldBase { private final String kind = "old"; private final String location;}
public final class A extends OldBase { private String a;}type A struct { // REQUIRED Kind *string
// REQUIRED Location *string A *string}4. Discriminator Preservation
Section titled “4. Discriminator Preservation”Discriminator properties are never dropped, even if the new base has a same-named property.
Common Use Cases
Section titled “Common Use Cases”Multi-Level Discriminated Hierarchy
Section titled “Multi-Level Discriminated Hierarchy”Use the decorator to create deeper inheritance in discriminated models:
@discriminator("kind")model Animal { kind: string; name: string;}
alias PetContent = { trained: boolean;};
model Pet extends Animal { kind: "pet"; ...PetContent;}
alias DogContent = { breed: string;};
@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(Pet)model Dog extends Animal { kind: "dog"; ...PetContent; ...DogContent;}// SDK result: Dog extends Pet. Dog.properties = { kind, breed }.// 'trained' is supplied by Pet → dropped. 'kind' is a discriminator → preserved.class Animal(_Model): kind: str name: str
class Pet(Animal, discriminator="pet"): kind: Literal["pet"] trained: bool
class Dog(Pet, discriminator="dog"): kind: Literal["dog"] breed: strpublic abstract partial class Animal{ public string Name { get; }}
public partial class Pet : Animal{ public bool Trained { get; }}
public partial class Dog : Pet{ public string Breed { get; }}export interface Animal { kind: string; name: string;}
export interface Pet extends Animal { kind: "pet"; trained: boolean;}
export interface Dog extends Pet { kind: "dog"; breed: string;}
export type AnimalUnion = PetUnion | Animal;export type PetUnion = Dog | Pet;public class Animal { private String kind = "Animal"; private final String name;}
public class Pet extends Animal { private String kind = "pet"; private final boolean trained;}
public final class Dog extends Pet { private String kind = "dog"; private final String breed;}type Animal struct { // REQUIRED Kind *string
// REQUIRED Name *string}
type Pet struct { // REQUIRED Kind *string
// REQUIRED Name *string
// REQUIRED Trained *bool}
type Dog struct { // REQUIRED Breed *string
// REQUIRED Kind *string
// REQUIRED Name *string
// REQUIRED Trained *bool}Brownfield ARM Resource Rebasing
Section titled “Brownfield ARM Resource Rebasing”A common pattern for Azure resources that need to be rebased onto TrackedResource:
model Resource { id?: string; name?: string; type?: string;}model TrackedResource extends Resource { location: string; tags?: Record<string>;}model Foo extends Resource { properties: FooProperties; location?: string; tags?: Record<string>;}@@Azure.ClientGenerator.Core.Legacy.hierarchyBuilding(Foo, TrackedResource);// SDK result: Foo extends TrackedResource. Foo.properties = { properties }.// location and tags are supplied by TrackedResource → dropped.// id, name, type are supplied by Resource (in TrackedResource's chain) → dropped.class Resource(_Model): id: Optional[str] name: Optional[str] type: Optional[str]
class TrackedResource(Resource): location: str tags: Optional[dict[str, str]]
class Foo(TrackedResource): properties: "FooProperties" location: Optional[str]public partial class Resource{ public string Id { get; } public string Name { get; } public string Type { get; }}
public partial class TrackedResource : Resource{ public string Location { get; } public virtual IDictionary<string, string> Tags { get; }}
public partial class Foo : TrackedResource{ public FooProperties Properties { get; } public new string Location { get; } public override IDictionary<string, string> Tags { get; }}export interface Resource { id?: string; name?: string; type?: string;}
export interface TrackedResource extends Resource { location: string; tags?: Record<string, string>;}
export interface Foo extends TrackedResource { properties: FooProperties; location?: string; tags?: Record<string, string>;}public class Resource { private String id; private String name; private String type;}
public class TrackedResource extends Resource { private final String location; private Map<String, String> tags;}
public final class Foo extends TrackedResource { private final FooProperties properties; private String location; private Map<String, String> tags;}type Foo struct { // REQUIRED Properties *FooProperties ID *string Location *string Name *string Tags map[string]*string Type *string}