Skip to content

Latest commit

 

History

History
664 lines (496 loc) · 26.6 KB

File metadata and controls

664 lines (496 loc) · 26.6 KB
title description author ms.author ms.topic
Upgrade guide to OpenAPI.NET 2.1
Learn how to upgrade your OpenAPI.NET version from 1.6 to 2.0
rachit.malik
malikrachit
conceptual

Introduction

We are excited to announce the new version of the OpenAPI.NET library!
OpenAPI.NET v2 is a major update to the OpenAPI.NET library. This release includes a number of performance improvements, API enhancements, and support for OpenAPI v3.1.

The biggest update ever

Since the release of the first version of the OpenAPI.NET library in 2018, there has not been a major version update to the library. With the addition of support for OpenAPI v3.1 it was necessary to make some breaking changes. With this opportunity, we have taken the time to make some other improvements to the library, based on the experience we have gained supporting a large community of users for the last six years .

Performance Improvements

One of the key features of OpenAPI.NET is its performance. This version makes it possible to parse JSON based OpenAPI descriptions even faster. OpenAPI.NET v1 relied on the excellent YamlSharp library for parsing both JSON and YAML files. With OpenAPI.NET v2 we are relying on System.Text.Json for parsing JSON files. For YAML files, we continue to use YamlSharp to parse YAML but then convert to JsonNodes for processing. This allows us to take advantage of the performance improvements in System.Text.Json while still supporting YAML files.

In v1, instances of $ref were resolved in a second pass of the document to ensure the target of the reference has been parsed before attempting to resolve it. In v2, reference targets are lazily resolved when reference objects are accessed. This improves load time performance for documents that make heavy use of references.

[How does this change the behaviour of external references?]

Results

The following benchmark results outline an overall 50% reduction in processing time for the document parsing as well as 35% reduction in memory allocation when parsing JSON. For YAML, the results between the different versions of the library are similar (some of the optimizations being compensated by the additional features).

1.X

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
PetStoreYaml 448.7 μs 326.6 μs 17.90 μs 58.5938 11.7188 - 381.79 KB
PetStoreJson 484.8 μs 156.9 μs 8.60 μs 62.5000 15.6250 - 389.28 KB
GHESYaml 1,008,349.6 μs 565,392.0 μs 30,991.04 μs 66000.0000 23000.0000 4000.0000 382785 KB
GHESJson 1,039,447.0 μs 267,501.0 μs 14,662.63 μs 67000.0000 23000.0000 4000.0000 389970.77 KB

2.X

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
PetStoreYaml 450.5 μs 59.26 μs 3.25 μs 58.5938 11.7188 - 377.15 KB
PetStoreJson 172.8 μs 123.46 μs 6.77 μs 39.0625 7.8125 - 239.29 KB
GHESYaml 943,452.7 μs 137,685.49 μs 7,547.01 μs 66000.0000 21000.0000 3000.0000 389463.91 KB
GHESJson 468,401.8 μs 300,711.80 μs 16,483.03 μs 41000.0000 15000.0000 3000.0000 250934.62 KB

Asynchronous API surface

Any method which results in input/output access (memory, network, storage) is now Async and returns a Task<Result> to avoid any blocking calls an improve concurrency.

For example:

var result = myOperation.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0);

Is now:

var result = await myOperation.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi2_0);

Trimming support

To better support applications deployed in high performance environments or on devices which have limited compute available, any usage of reflection has been removed from the code base. This also brings support for trimming to the library. Any method relying on reflection has been removed or re-written.

Note: as part of this change, the following types have been removed:

  • StringExtensions

Collections are not initialized

To lower the memory footprint of the library, collections are now NOT initialized anymore when instantiating any of the models.

Example

var mySchema = new OpenApiSchema();

// 1.6: works
// 2.X: if null reference types is enabled in the target application,
//      this will lead to a warning or error at compile time.
//      And fail at runtime with a null reference exception.
mySchema.AnyOf.Add(otherSchema);

// one solution
mySchema.AnyOf ??= new List<IOpenApiSchema>();
mySchema.AnyOf.Add(otherSchema);

// alternative
mySchema.AnyOf = new List<IOpenApiSchema> { otherSchema };

Reduced Dependencies

In OpenAPI v1, it was necessary to include the Microsoft.OpenApi.Readers library to be able to read OpenAPI descriptions in either YAML or JSON. In OpenAPI.NET v2, the core Microsoft.OpenAPI library can both read and write JSON. It is only necessary to use the newly renamed Microsoft.OpenApi.YamlReader library if you need YAML support. This allows teams who are only working in JSON to avoid the additional dependency and therefore eliminate all non-.NET library references.

Once the dependency is added, the reader needs to be added to the reader settings as demonstrated below

var settings = new OpenApiReaderSettings();  
settings.AddYamlReader();  

var result = OpenApiDocument.LoadAsync(openApiString, settings: settings); 

API Enhancements

Loading the document

The v1 library attempted to mimic the pattern of XmlTextReader and JsonTextReader for the purpose of loading OpenAPI documents from strings, streams and text readers.

var reader = new OpenApiStringReader();
var openApiDoc = reader.Read(stringOpenApiDoc, out var diagnostic);

The same pattern can be used for OpenApiStreamReader and OpenApiTextReader. When we introduced the ReadAsync methods we eliminated the use of the out parameter. To improve code readability, we've added deconstruction support to ReadResult. The properties also have been renamed to avoid confusion with their types.

var reader = new OpenApiStreamReader();
var (document, diagnostics) = await reader.ReadAsync(streamOpenApiDoc);
// or
var result = await reader.ReadAsync(streamOpenApiDoc);
var document = result.Document;
var diagnostics = result.Diagnostics;

A ReadResult object acts as a tuple of OpenApiDocument and OpenApiDiagnostic.

The challenge with this approach is that the reader classes are not very discoverable and the behaviour is not actually consistent with the *TextReader pattern that allows incrementally reading the document. This library does not support incrementally reading the OpenAPI Document. It only reads a complete document and returns an OpenApiDocument instance.

In the v2 library we are moving to the pattern used by classes like XDocument where a set of static Load and Parse methods are used as factory methods.

public class OpenApiDocument {
    public static async Task<ReadResult> LoadAsync(string url, OpenApiReaderSettings settings = null) {}
    public static async Task<ReadResult> LoadAsync(Stream stream, string? format = null, OpenApiReaderSettings? settings = null) {}
    public static ReadResult Load(MemoryStream stream, string? format = null, OpenApiReaderSettings? settings = null) {}
    public static ReadResult Parse(string input, string? format = null, OpenApiReaderSettings? settings = null) {}
}

This API design allows a developer to use IDE autocomplete to present all the loading options by simply knowing the name of the OpenApiDocument class. Each of these methods are layered on top of the more primitive methods to ensure consistent behaviour.

As the YAML format is only supported when including the Microsoft.OpenApi.YamlReader library it was decided not to use an enum for the format parameter. We are considering implementing a more strongly typed solution similar to the way that HttpMethod is implemented so that we have a strongly typed experience that is also extensible.

When the loading methods are used without a format parameter, we will attempt to parse the document using the default JSON reader. If that fails and the YAML reader is registered, then we will attempt to read as YAML. The goal is always to provide the fastest path with JSON but still maintain the convenience of not having to care whether a URL points to YAML or JSON if you need that flexibility.

Additional exceptions

While parsing an OpenAPI description, the library will now throw the following new exceptions:

  • OpenApiReaderException when the reader for the format cannot be found, the document cannot be parsed because it does not follow the format conventions, etc...
  • OpenApiUnsupportedSpecVersionException when the document's version is not implemented by this version of the library and therefore cannot be parsed.

Removing the OpenAPI Any classes

In the OpenAPI specification, there are a few properties that are defined as type any. This includes:

  • the example property in the parameter, media type objects
  • the value property in the example object
  • the values in the link object's parameters dictionary and requestBody property
  • all x- extension properties

In the v1 library, there are a set of classes that are derived from the OpenApiAny base class which is an abstract model which reflects the JSON data model plus some additional primitive types such as decimal, float, datetime etc.

In v2 we are removing this abstraction and relying on the JsonNode model to represent these inner types. In v1 we were not able to reliably identify the additional primitive types and it caused a significant amount of false negatives in error reporting as well as incorrectly parsed data values.

Due to JsonNode implicit operators, this makes initialization sometimes easier, instead of:

new OpenApiParameter
{
    In = null,
    Name = "username",
    Description = "username to fetch",
    Example = new OpenApiFloat(5),
};

the assignment becomes simply,

    Example = 0.5f,

For a more complex example, where the developer wants to create an extension that is an object they would do this in v1:

var openApiObject = new OpenApiObject
{
    {"stringProp", new OpenApiString("stringValue1")},
    {"objProp", new OpenApiObject()},
    {
        "arrayProp",
        new OpenApiArray
        {
            new OpenApiBoolean(false)
        }
    }
};
var parameter = new OpenApiParameter();
parameter.Extensions.Add("x-foo", openApiObject);

In v2, the equivalent code would be,

var openApiObject = new JsonObject
{
    {"stringProp", "stringValue1"},
    {"objProp", new JsonObject()},
    {
        "arrayProp",
        new JsonArray
        {
            false
        }
    }
};
var parameter = new OpenApiParameter();
parameter.Extensions.Add("x-foo", new JsonNodeExtension(openApiObject));

Note: as part of this change, the following types have been removed from the library:

  • AnyType
  • IOpenApiAny
  • OpenApiAnyCloneHelper
  • OpenApiArray
  • OpenApiBinary
  • OpenApiBoolean
  • OpenApiByte
  • OpenApiDate
  • OpenApiDateTime
  • OpenApiDouble
  • OpenApiFloat
  • OpenApiInteger
  • OpenApiLong
  • OpenApiNull
  • OpenApiObject
  • OpenApiPassword
  • OpenApiPrimitive
  • OpenApiString
  • PrimitiveType

Enable Null Reference Type Support

Version 2.0 introduces support for null reference types, which improves type safety and reduces the likelihood of null reference exceptions.

Example:

var document = new OpenApiDocument
{
    Components = null
};

// 1.X: no compilation error or warning, but fails with a null reference exception at runtime
// 2.X: compilation error or warning depending on the project configuration
var componentA = document.Components["A"];

Ephemeral object properties are now in Metadata

In version 1.X applications could add ephemeral properties to some of the models from the libraries. These properties would be carried along in an "Annotations" property, but not serialized. This is especially helpful when building integrations that build document in multiple phases and need additional context to complete the work. The property is now named metadata to avoid any confusion with other terms. The parent interface has also been renamed from IOpenApiAnnotatable to IMetadataContainer.

var schema = new OpenApiSchema();

// 1.X
var info = schema.Annotations["foo"];
// 2.X
var info = schema.Metadata["foo"];

Updates to OpenApiSchema

The OpenAPI 3.1 specification changes significantly how it leverages JSON Schema. In 3.0 and earlier, OpenAPI used a "subset, superset" of JSON Schema draft-4. This caused many problems for developers trying to use JSON Schema validation libraries with the JSON Schema in their OpenAPI descriptions. In OpenAPI 3.1, the 2020-12 draft version of JSON Schema was adopted and a new JSON Schema vocabulary was adopted to support OpenAPI specific keywords. All attempts to constrain what JSON Schema keywords could be used in OpenAPI were removed.

New keywords introduced in 2020-12

/// $schema, a JSON Schema dialect identifier. Value must be a URI
public string Schema { get; set; }
/// $id - Identifies a schema resource with its canonical URI.
public string Id { get; set; }
/// $comment - reserves a location for comments from schema authors to readers or maintainers of the schema.
public string Comment { get; set; }
/// $vocabulary- used in meta-schemas to identify the vocabularies available for use in schemas described by that meta-schema.
public IDictionary<string, bool> Vocabulary { get; set; }
/// $dynamicRef - an applicator that allows for deferring the full resolution until runtime, at which point it is resolved each time it is encountered while evaluating an instance
public string DynamicRef { get; set; }
/// $dynamicAnchor - used to create plain name fragments that are not tied to any particular structural location for referencing purposes, which are taken into consideration for dynamic referencing.
public string DynamicAnchor { get; set; }
/// $defs - reserves a location for schema authors to inline re-usable JSON Schemas into a more general schema.
public IDictionary<string, OpenApiSchema> Definitions { get; set; }
public IDictionary<string, OpenApiSchema> PatternProperties { get; set; } = new Dictionary<string, OpenApiSchema>();
public bool UnevaluatedProperties { get; set;}

Changes to existing keywords

public string? ExclusiveMaximum { get; set; }  // type changed to reflect the new version of JSON schema
public string? ExclusiveMinimum { get; set; } // type changed to reflect the new version of JSON schema
public JsonSchemaType? Type { get; set; }  // Was string, now flagged enum
public string? Maximum { get; set; }      // type changed to overcome double vs decimal issues
public string? Minimum { get; set; }       // type changed to overcome double vs decimal issues

public JsonNode Default { get; set; }  // Type matching no longer enforced. Was IOpenApiAny
public bool ReadOnly { get; set; }  // No longer has defined semantics in OpenAPI 3.1
public bool WriteOnly { get; set; }  // No longer has defined semantics in OpenAPI 3.1

public JsonNode Example { get; set; }  // No longer IOpenApiAny
public IList<JsonNode> Examples { get; set; }
public IList<JsonNode> Enum { get; set; }
public OpenApiExternalDocs ExternalDocs { get; set; }  // OpenApi Vocab
public bool Deprecated { get; set; }  // OpenApi Vocab
public OpenApiXml Xml { get; set; }  // OpenApi Vocab

public IDictionary<string, object> Metadata { get; set; }  // Custom property bag to be used by the application, used to be named annotations

OpenApiSchema methods

Other than the addition of SerializeAsV31, the methods have not changed.

public class OpenApiSchema : IMetadataContainer, IOpenApiExtensible, IOpenApiReferenceable, IOpenApiSerializable
{
    public OpenApiSchema() { }
    public OpenApiSchema(OpenApiSchema schema) { }
    public void SerializeAsV31(IOpenApiWriter writer) { }
    public void SerializeAsV3(IOpenApiWriter writer) { }
    public void SerializeAsV2(IOpenApiWriter writer) { }
}

OpenAPI v3.1 Support

There are a number of new features in OpenAPI v3.1 that are now supported in OpenAPI.NET.

JsonSchema Dialect and BaseUri in OpenApiDocument

To enable full compatibility with JSON Schema, the OpenApiDocument class now supports a JsonSchemaDialect property. This property specifies the JSON Schema dialect used throughout the document, using a URI. By explicitly declaring the dialect, tooling can be directed to use a JSON Schema version other than the default 2020-12 draft. However, OpenAPI.NET does not guarantee compatibility with versions other than 2020-12.

In addition, a BaseUri property has been added to represent the identity of the OpenAPI document. If the document’s identity is not provided or cannot be determined at based on its location, this property will be set to a generated placeholder URI.

/// <summary>
/// Describes an OpenAPI object (OpenAPI document). See: https://spec.openapis.org
/// </summary>
public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible, IMetadataContainer
{
    /// <summary>
    /// The default value for the $schema keyword within Schema Objects contained within this OAS document. This MUST be in the form of a URI.
    /// </summary>
    public Uri? JsonSchemaDialect { get; set; }

    /// <summary>
    /// Absolute location of the document or a generated placeholder if location is not given
    /// </summary>
    public Uri BaseUri { get; internal set; }
}

Webhooks

public class OpenApiDocument  : IOpenApiSerializable, IOpenApiExtensible, IOpenApiMetadataContainer
{
    public IDictionary<string, OpenApiPathItem>? Webhooks { get; set; } = new Dictionary<string, OpenApiPathItem>();
}

Summary in info object

public class OpenApiInfo : IOpenApiSerializable, IOpenApiExtensible
{
    /// <summary>
    /// A short summary of the API.
    /// </summary>
    public string Summary { get; set; }
}

License SPDX identifiers

/// <summary>
/// License Object.
/// </summary>
public class OpenApiLicense : IOpenApiSerializable, IOpenApiExtensible
{
    /// <summary>
    /// An SPDX license expression for the API. The identifier field is mutually exclusive of the Url property.
    /// </summary>
    public string Identifier { get; set; }
}

Reusable path items

/// <summary>
/// Components Object.
/// </summary>
public class OpenApiComponents : IOpenApiSerializable, IOpenApiExtensible
{
    /// <summary>
    /// An object to hold reusable <see cref="OpenApiPathItem"/> Object.
    /// </summary>
    public IDictionary<string, OpenApiPathItem>? PathItems { get; set; }
}

Summary and Description alongside $ref

Through the use of proxy objects in order to represent references, it is now possible to set the Summary and Description property on an object that is a reference. This was previously not possible.

var parameter = new OpenApiParameterReference("id", hostDocument)
{
    Description = "Customer Id"
};

Once serialized results in:

$ref: id
description: Customer Id

Use HTTP Method Object Instead of Enum

HTTP methods are now represented as objects instead of enums. This change enhances flexibility but requires updates to how HTTP methods are handled in your code. Example:

// Before (1.6)
OpenApiOperation operation = new OpenApiOperation
{
    HttpMethod = OperationType.Get
};

// After (2.0)
OpenApiOperation operation = new OpenApiOperation
{
    HttpMethod = new HttpMethod("GET") // or HttpMethod.Get
};

References as Components

References can now be used as components, allowing for more modular and reusable OpenAPI documents.

Example:

// Before (1.6)
OpenApiSchema schema = new OpenApiSchema
{
    Reference = new OpenApiReference
    {
        Type = ReferenceType.Schema,
        Id = "MySchema"
    }
};

// After (2.0)
OpenApiComponents components = new OpenApiComponents
{
    Schemas = new Dictionary<string, IOpenApiSchema>
    {
        ["MySchema"] = new OpenApiSchemaReference("MyOtherSchema")
        {
            Description = "Other reusable schema from initial schema"
        }
    }
};

OpenApiDocument.SerializeAs()

The SerializeAs() method simplifies serialization scenarios, making it easier to convert OpenAPI documents to different formats.

Example:

OpenApiDocument document = new OpenApiDocument();
string json = await document.SerializeAsync(OpenApiSpecVersion.OpenApi3_0, OpenApiConstants.Json);

Use OpenApiConstants string Instead of OpenApiFormat Enum

OpenApiConstants are now used instead the OpenApiFormat enumeration.

Example:

// Before (1.6)
var outputString = openApiDocument.Serialize(OpenApiSpecVersion.OpenApi2_0, OpenApiFormat.Json); 

// After (2.0)
var outputString = await openApiDocument.SerializeAsync(OpenApiSpecVersion.OpenApi2_0, OpenApiConstants.Json);

OpenApiSchema's Type property is now a flaggable enum

In v2.0, the Type property in OpenApiSchema is now defined as a flaggable enum, allowing consumers to swap nullable for type arrays.

Example:

// v1.6.x
var schema = new OpenApiSchema
{
    Type = "string",
    Nullable = true
}

// v2.0
// bitwise OR(|) - combines flags to allow multiple types
var schema = new OpenApiSchema
{
    Type = JsonSchemaType.String | JsonSchemaType.Null 
}

// bitwise NOT(~) - inverts bits; filters out null flag
var schema = new OpenApiSchema
{
    Type = JsonSchemaType.String & ~JsonSchemaType.Null
}

// bitwise AND(&) - intersects flags to check for a specific type
var schema = new OpenApiSchema
{
    Type = (JsonSchemaType.String & JsonSchemaType.Null) == JsonSchemaType.Null 
}

Component registration in a document's workspace

When loading up a file into an in-memory document, all the components contained in the document are registered within the document's workspace by default to aid with reference resolution. However if you're working directly with a DOM and you need the references resolved, you can register the components as below:

// register all components
document.Workspace.RegisterComponents(OpenApiDocument document);

// register single component
document.AddComponent<T>(string id, T componentToRegister);

Refactored model architecture

The following structural improvements have been made to the OpenAPI model layer to enhance type safety, extensibility, and maintainability:

  1. Model Interfaces Introduced: Each model now has a corresponding interface (e.g., IOpenApiSchema for OpenApiSchema). This allows for better abstraction and testing support, while also simplifying cross-cutting concerns like serialization.

  2. Models as Reference Types: All models are now implemented as reference types to ensure consistent identity semantics, especially when managing circular references or shared definitions.

  3. Type Assertion Pattern Adopted: A standardized pattern has been introduced for casting model instances to specific types safely and predictably, reducing the risk of invalid casts or reflection-based logic.

  4. Removed Reference Fields from Base Models: Properties like Reference that were previously defined on base model types have been removed. Models that support referencing now handle this behavior explicitly via composition rather than inheritance.

  5. New Target and RecursiveTarget Properties: A Target property that points to the actual resolved model instance as well as a RecursiveTarget property that handles recursive references and supports advanced dereferencing logic have been introduced.

  6. Removed OpenApiReferenceResolver: This resolver class has been removed in favor of a more streamlined resolution model using the Target and RecursiveTarget properties along with updated reader/serializer pipelines.

Visitor and Validator now pass an interface model

Example:

//v1.6.x
public override void Visit(OpenApiParameter parameter){}

//v2.0
public override void Visit(IOpenApiParameter parameter){}

Cleaned up the IEffective/GetEffective infrastructure

All the IEffective and GetEffective infrastructure in the models have been removed as we've implemented lazy reference resolution using the proxy design.

Shallow Copy in place of copy constructors

Copy constructors for referenceable components have been made internal, a new CreateShallowCopy() method has been exposed on these models to facilitate cloning.

Example:

var schema = new OpenApiSchema();
var schemaCopy = schema.CreateShallowCopy();

Duplicated _style Property on Parameter Removed

The redundant _style property on the Parameter model has been removed to simplify the model's structure.

Discriminator now use References

Discriminator mappings have been updated from using a Dictionary<string, string> to a Dictionary<string, OpenApiSchemaReference>. This change improves the handling of discriminator mappings by referencing OpenAPI schema components more explicitly, which enhances schema resolution.

Example:

// v1.6.x
Discriminator = new()
{
    PropertyName = "@odata.type",
    Mapping = new Dictionary<string, string> {
        {
            "#microsoft.graph.directoryObject", "#/components/schemas/microsoft.graph.directoryObject"
        }
    }
}

//v2.0
Discriminator = new()
{
    PropertyName = "@odata.type",
    Mapping = new Dictionary<string, OpenApiSchemaReference> {
        {
            "#microsoft.graph.directoryObject", new OpenApiSchemaReference("microsoft.graph.directoryObject")
        }
    }
}

Bug Fixes

Serialization of References

Fixed a bug where references would not serialize summary or descriptions in OpenAPI 3.1. Example:

OpenApiSchemaReference schemaRef = new OpenApiSchemaReference("MySchema")
{
    Summary = "This is a summary",
    Description = "This is a description"
};

Feedback

If you have any feedback please file a GitHub issue here The team is looking forward to hear your experience trying the new version and we hope you have fun busting out your OpenAPI 3.1 descriptions.