Skip to content

Latest commit

 

History

History
703 lines (642 loc) · 20 KB

defining-clients-swagger.md

File metadata and controls

703 lines (642 loc) · 20 KB

#Working with Swagger Specifications

AutoRest processes Swagger specifications following the Swagger RESTful API Documentation Specification API descriptions that are valid according to the schema can produce client libraries that are not very user-friendly. Here are some techniques to apply in authoring Swagger that improve the usability of the client generated by AutoRest.

Contents

Data Types

Primitive Data Types

The primitive types are based on JSON-Schema Draft 4. More details in the Swagger Specification here.

Example:

"defitions": {
  "pet": {
    "properties": {
      "name": {
        "type": "string"
	  },
	  "age": {
	    "type": "integer"
	  }
	}
  }
}

Generates C# client model type:

public partial class Pet
{
    private int? _age;

    /// <summary>
    /// Optional.
    /// </summary>
    public int? Age
    {
        get { return this._age; }
        set { this._age = value; }
    }

    private string _name;

    /// <summary>
    /// Optional.
    /// </summary>
    public string Name
    {
        get { return this._name; }
        set { this._name = value; }
    }

    /// <summary>
    /// Initializes a new instance of the Pet class.
    /// </summary>
    public Pet()
    {
    }
}

byte[], DateTimeOffset, int, long

  • byte[] To represent byte arrays in the generated code, the property of the Swagger definition should have string as its type and byte as its format. This indicates binary data that will be represented as a base64-encoded string in requests and responses. The generated client will automatically do this encoding when processing requests and responses.

  • DateTimeOffset AutoRest generates DateTimeOffset typed properties in generated C# code for Swagger properties that have string as the type and date-time as the format.

  • int / long Both int and long proeprties in the generated code correspond to integer types in Swagger properties. If the format of the Swagger property is int32, int will be generated; if the format is int64, long will be generated. If the format field of the Swagger property is not set, AutoRest use format int32.

Example:

"pet": {
  "properties": {
    "age": {
      "type": "integer",
      "format": "int32"
    },
    "number": {
      "type": "integer",
      "format": "int64"
    },
    "name": {
      "type": "string",
      "format": "byte"
    },
    "birthday": {
      "name": "dateTime",
      "type": "string",
      "format": "date-time"
    }
  }
}

Generates C# client model type:

public partial class Pet
{
    private int? _age;

    /// <summary>
    /// Optional.
    /// </summary>
    public int? Age
    {
        get { return this._age; }
        set { this._age = value; }
    }

    private DateTimeOffset? _birthday;

    /// <summary>
    /// Optional.
    /// </summary>
    public DateTimeOffset? Birthday
    {
        get { return this._birthday; }
        set { this._birthday = value; }
    }

    private byte[] _name;

    /// <summary>
    /// Optional.
    /// </summary>
    public byte[] Name
    {
        get { return this._name; }
        set { this._name = value; }
    }

    private long? _number;

    /// <summary>
    /// Optional.
    /// </summary>
    public long? Number
    {
        get { return this._number; }
        set { this._number = value; }
    }

    /// <summary>
    /// Initializes a new instance of the Pet class.
    /// </summary>
    public Pet()
    {
    }
}

Arrays and Sequences

AutoRest builds sequences from schemas with type array. The following definition

"pet": {
  "properties": {
    "names": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}

Generates C# client model type

public partial class Pet
{
    private IList<string> _names;

    /// <summary>
    /// Optional.
    /// </summary>
    public IList<string> Names
    {
        get { return this._names; }
        set { this._names = value; }
    }

    /// <summary>
    /// Initializes a new instance of the Pet class.
    /// </summary>
    public Pet()
    {
        this.Names = new LazyList<string>();
    }
}

Dictionaries

AutoRest generates dictionaries (or hash maps in some contexts) using additionalProperites from JSON-Schema Draft 4. The additionalProperties element should specify the Swagger schema of the values in the dictionary . The keys of the generated dictionary will be of type string.

The following definition

"StringDictionary": {
  "additionalProperties": {
    "type": "string"
  }
}

will generate C# client library

public static partial class StringDictionary
{
    /// <summary>
    /// Deserialize the object
    /// </summary>
    public static IDictionary<string, string> DeserializeJson(JToken inputObject)
    {
        IDictionary<string, string> deserializedObject = new Dictionary<string, string>();
        foreach (JProperty property in inputObject)
        {
            deserializedObject.Add(((string)property.Name), ((string)property.Value));
        }
        return deserializedObject;
    }
}

In the example for Sequences, the Pet is a POCO model while in this example the StringDictionary only generates a static class helper for deserialization. A model type is generated if the corresponding Swagger schema is of type 'object' or has one or more 'properties'. A static helper class is generated if the corresponding Swagger schema specifies a primitive type or 'array'. This rule applies to all schemas in both parameters and responses.

Inheritance and Polymorphism

Inheritance

AutoRest builds inheritance between types if an allOf field is specified in a Swagger definition with ONLY one reference to another Swagger definition. The following example demonstrate a Cat type inheriting a Pet with its allOf set to [{"$ref": "Pet"}].

Note: Only allOf fields with one schema reference will be treated as inheritance. If allOf contains more than one schema that has "$ref" as the key, the properties from the referenced schemas will be composed without inheritance. However, if an allOf contains multiple inline schemas and a single schema reference, the generated model type will use inheritance.

Example:

"Pet": {
  "properties": {
    "name": {
      "type": "string"
    }
  }
},
"Cat": {
  "allOf": [ { "$ref":  "Pet" } ],
  "properties": {
    "color": {
      "type": "string",
      "description": "cat color"
    }
  }
}

will generate C# model types

public partial class Cat : Pet
{
    private string _color;

    /// <summary>
    /// Optional. cat color
    /// </summary>
    public string Color
    {
        get { return this._color; }
        set { this._color = value; }
    }

    /// <summary>
    /// Initializes a new instance of the Cat class.
    /// </summary>
    public Cat()
    {
    }

    /// <summary>
    /// Serialize the object
    /// </summary>
    /// <returns>
    /// Returns the json model for the type Cat
    /// </returns>
    public override JToken SerializeJson(JToken outputObject)
    {
        outputObject = base.SerializeJson(outputObject);
        if (outputObject == null)
        {
            outputObject = new JObject();
        }
        if (this.Color != null)
        {
            outputObject["color"] = this.Color;
        }
        return outputObject;
    }

    /// <summary>
    /// Deserialize the object
    /// </summary>
    public override void DeserializeJson(JToken inputObject)
    {
        base.DeserializeJson(inputObject);
        if (inputObject != null && inputObject.Type != JTokenType.Null)
        {
            JToken colorValue = inputObject["color"];
            if (colorValue != null && colorValue.Type != JTokenType.Null)
            {
                this.Color = ((string)colorValue);
            }
        }
    }
}

public partial class Pet
{
    private string _name;

    /// <summary>
    /// Optional.
    /// </summary>
    public string Name
    {
        get { return this._name; }
        set { this._name = value; }
    }

    /// <summary>
    /// Initializes a new instance of the Pet class.
    /// </summary>
    public Pet()
    {
    }

    /// <summary>
    /// Serialize the object
    /// </summary>
    /// <returns>
    /// Returns the json model for the type Pet
    /// </returns>
    public virtual JToken SerializeJson(JToken outputObject)
    {
        if (outputObject == null)
        {
            outputObject = new JObject();
        }
        if (this.Name != null)
        {
            outputObject["name"] = this.Name;
        }
        return outputObject;
    }

    /// <summary>
    /// Deserialize the object
    /// </summary>
    public virtual void DeserializeJson(JToken inputObject)
    {
        if (inputObject != null && inputObject.Type != JTokenType.Null)
        {
            JToken nameValue = inputObject["name"];
            if (nameValue != null && nameValue.Type != JTokenType.Null)
            {
                this.Name = ((string)nameValue);
            }
        }
    }
}

In Cat's serialization and deserialization methods, Pet's corresponding methods are called first to build all the properties of Pet.

Polymorphism

To describe polymorphic inheritance between types, Swagger uses an extra "discriminator" field to indicate the exact serialization of the object on the wire. To make a set of classes polymorphic, use 'allOf' with a schema reference to indicate inheritance from a base schema and a discriminator field to the base schema. In the example above, adding a discriminator field named objectType to Pet will make the genereated set of classes polymorphic:

"Pet": {
  "discriminator": "Type",
  "required": [
    "objectType"
  ],
  "properties": {
    "name": {
      "type": "string"
    },
    "objectType": {
      "type": "string"
    }
  }
}

The generated models in C# code are nearly identical, but the base serialization and deserialization methods are changed as follows:

public async Task<HttpOperationResponse<Pet>> GetPolymorphicPetsWithOperationResponseAsync(CancellationToken cancellationToken)
{

............

	// Serialize Request
	string requestContent = null;
	JToken requestDoc = petCreateOrUpdateParameter.SerializeJson(null);
	if (petCreateOrUpdateParameter is Cat)
	{
	    requestDoc["ObjectType"] = "Cat";
	}
	else if (petCreateOrUpdateParameter is Dog)
	{
	    requestDoc["ObjectType"] = "Dog";
	}
	else
	{
	    requestDoc["ObjectType"] = "Pet";
	}

............

	// Deserialize Response
	if (statusCode == HttpStatusCode.OK)
	{
	    Pet resultModel = new Pet();
	    JToken responseDoc = null;
	    if (string.IsNullOrEmpty(responseContent) == false)
	    {
	        responseDoc = JToken.Parse(responseContent);
	    }
	    if (responseDoc != null)
	    {
	        string typeName = ((string)responseDoc["ObjectType"]);
	        if (typeName == "Cat")
	        {
	            resultModel = new Cat();
	        }
	        else if (typeName == "Dog")
	        {
	            resultModel = new Dog();
	        }
	        else
	        {
	            resultModel = new Pet();
	        }
	        resultModel.DeserializeJson(responseDoc);
	    }
	    result.Body = resultModel;
	}

............

Type Name Generation

Type name generation is straightforward if a Swagger schema is defined in the "#/definitions" block. The name of the Swagger Schema will be respected in the corresponding generated model type, like the Pet model in the examples above. Invalid characters will be filtered, and type capitalization is changed to comply with language-specific coding guidelines, but the generated model name should make sense if the one in the Swagger definitions list makes sense.

Type name generation becomes tricky in inline schema definitions. There are three scenarios when AutoRest generates a name on its own. The names are generated using the context of the schema in the Swagger specification so that the correlation between model type and swagger schema is easy to find. A generated type name can easily be changed by moving the corresponding schema into the "#/definitions" list and referencing the schema from the parameters and responses where it appears in the Swagger specification.

  • Inline parameters A Schema defined inside a body parameter. The parameter name will be used for the generated type name. The following example will generate a model type named Style.
"parameters": [
  {
    "in": "body",
    "name": "style",
    "schema": {
      "properties": {
        "name": {
          "type": "string"
        },
        "color": {
          "type": "string"
        }
      }
    }
  }
]
  • Inline responses A response with a schema definition inside. The model type name will be operationId + http status code + "Response". The following example will generate a type AddPetOkResponse.
......
"operationId": "addPet",
......
"200": {
  "description": "pet response",
  "schema": {
    "properties": {
      "id": {
        "type": "integer",
        "format": "int64"
      },
      "name": {
        "type": "string"
      },
    }
  }
}
  • Inline properties A property of a reference type contains an inline Swagger schema definition. The type name for the generated model is composed of the parent class's type name concatenated with the property's name. The following example will generate a type PetStyle.
"Pet": {
  "properties": {
    "style": {
      "properties": {
        "name": {
          "type": "string"
        },
        "color": {
          "type": "string"
        }
      }
    }
  }
}
  • Schemas in sequences and dictionaries A schema defined in the 'items' proeprty of a sequence or the 'additionalProperties' value of a dictionary. Model types corresponding to Items of a sequence are named using the parent class's name concatenated with "Item". Model types corresponding to the 'additinalPropeties' value of a dictionary are named using the parent class's name concatenated with "Value". The following example will generate types PetFavFoodItem and PetFavFoodBrandValue.
"Pet": {
  "properties": {
    "fav_food": {
	  "type": "array",
	  "items": {
	    "properties": {
	      "name": {
	        "type": "string"
	      },
	      "taste": {
	        "type": "string"
	      }
	    }
	  }
    },
    "fav_food_brand": {
      "additionalProperties": {
	    "properties": {
	      "manufacturer": {
	        "type": "string"
	      }
	    }
      }
    }
  }
}

Operations

Generating Operation Classes

In many cases, client operations are intended to be grouped by resource type for better usability. AutoRest supports categorizing operations using _ in the operationId field of a SwaggerOperation. The part appearing before _ will be treated as the operations class name, and the part after will be treated as the method name.

Example: The following Swagger specification:

"paths": {
  "/api/Values/{id}": {
    "get": {
      "tags": [
        "Values"
      ],
      "operationId": "Values_Get",
............

will generate a Get method inside a Values class. The end user will access the method by calling client.Values.Get(). This is a neat way of organizing your client if you have multiple operations with the same operation name but different underlying resources.

If the -OutputFile parameter is not specified when invoking the AutoRest Command Line Interface, generated files will also be organized by namespaces. If you have operationIds ns1_get and ns2_get, you will have ns1.cs and ns2.cs in the generated C# client library.

Specifying required parameters and properties

Parameters and properties in Swagger schema use different notations and conventions to determine if they are required or optional.

Parameters in the 'path' or 'body' are always required. Parameters may also use a 'required' Boolean property to indicate that they are required for the operation, as in the example shown below.

"parameters": [
  {
    "name": "subscriptionId",
    "in": "path",
    "required": true,
    "type": "integer"
  },
  {
    "name": "resourceGroupName",
    "in": "path",
    "type": "string"
  },
  {
    "name": "api-version",
    "in": "query",
    "required": false,
    "type": "integer"
  }
]

Generates C# client side method of

public async Task<HttpOperationResponse<Product>> ListWithOperationResponseAsync(int subscriptionId, string resourceGroupName, int? apiVersion, CancellationToken cancellationToken)
{
    // Validate
    if (resourceGroupName == null)
    {
        throw new ArgumentNullException("resourceGroupName");
    }
............

where a non-nullable type is changed into its nullable wrapper if the corresponding parameter is optional and a validation is added if a nullable type is marked as required.

Note that parameters that have field in as path are always required and the required field will be ignored.

Properties in SwaggerSchema do not contain a required field. Instead, Each definition schema can provide a 'required' array that specifies which proeprties are required. An example is shown below.

"Product": {
  "required": [
    "product_id", "display_name"
  ],
  "properties": {
    "product_id": {
      "type": "string"
    },
    "description": {
      "type": "string"
    },
    "display_name": {
      "type": "string"
    },
    "capacity": {
      "type": "string"
    },
    "image": {
      "type": "string"
    }
  }
}

Error Modeling

At runtime, if the server returns an unexpected status code, the generated client throws an exception of type HttpOperationException. The exception instance will contain the request of type HttpRequestMessage (in property Request), the response of type HttpResponseMessage (in property Response), and the error model (in property Body). The error model must be defined as the schema of the default response.

Example: A response of

"default": {
  "description": "Unexpected error",
  "schema": {
    "$ref": "Error"
  }
}

together with its definition

"Error": {
  "properties": {
    "code": {
      "type": "integer",
      "format": "int32"
    },
    "message": {
      "type": "string"
    },
    "fields": {
      "type": "string"
    }
  }
}

Generates the following error handling code:

if (statusCode != HttpStatusCode.OK) // and more if more acceptable status codes
{
    Error errorModel = new Error();
    JToken responseDoc = null;
    if (string.IsNullOrEmpty(responseContent) == false)
    {
        responseDoc = JToken.Parse(responseContent);
    }
    if (responseDoc != null)
    {
        errorModel.DeserializeJson(responseDoc);
    }
    HttpOperationException<Error> ex = new HttpOperationException<Error>();
    ex.Request = httpRequest;
    ex.Response = httpResponse;
    ex.Body = errorModel;
    if (shouldTrace)
    {
        ServiceClientTracing.Error(invocationId, ex);
    }
    throw ex;
}

See Error Handling for details on how to catch and use the exceptions from generated clients.