microsoft / OpenAPI.NET

The OpenAPI.NET SDK contains a useful object model for OpenAPI documents in .NET along with common serializers to extract raw OpenAPI JSON and YAML documents from the model.
MIT License
1.38k stars 231 forks source link

Make it possible to have a deterministic order in serialized OpenApi files #1314

Open gturri opened 1 year ago

gturri commented 1 year ago

Is your feature request related to a problem? Please describe. I use OpenAPI.NET to serialize instances of OpenApiDocument and store them in a git repo. So, ideally I would like that when I make a simple change to my API, I get a simple change in the serialized document, in order to have an easy to review git diff.

It is not the case currently, because serialization iterates on some Dictionary and that the order of the iteration is hence not deterministic. For instance, if I add one endpoint to my API, I would like that my serialized OAS file looks like what it was at my previous generation, with just a few added lines. But in practice I may have more diffs, because my paths and my schemas may end up serialized in a different order.

Describe the solution you'd like I can already achieve part of what I would like from my client code in the sense that I can get a deterministic order for the Schemas section, if I do

OpenApiDocument doc = BuildMyDoc();
doc.Components.Schemas = new SortedList<string, OpenApiSchema>(doc.Component.Schemas) // <-- that's the important line
doc.SerializeAsV3(new OpenApiYamlWriter(....));

It works rather fine, in the sense that it leads to a serialized schemas section where all schemas are order alphabetically (making it nice for my git diffs, and making the file easier to browse overall). However, it does not feel really "clean" in itself, and this approach does not work for other parts of the document. In particular for the paths; because they are represented by an OpenApiPaths which ends up extending a Dictionary and I could not find a virtual method or an attribute I could override to achieve a similar deterministic order.

Given this context, a solution that would be nice would be to edit IOpenApiSerializable in order to turn SerializeAsV3(IOpenApiWriter writer) into SerializeAsV3(IOpenApiWriter writer, bool enforceAlphabeticalOrder = false). So for instance OpenApiExtensibleDictionary.SerializeAsV3 could be implemented like

var items = enforceAlphabeticalOrder ? new SortedList<string, T>(this) : this;
foreach(var item in items) {
  ...
}

Having this feature behind a boolean enforceAlphabeticalOrder could make it possible for users interested in this feature to use it, and users not interested in it would not suffer any performance penalty by leaving it to false.

Describe alternatives you've considered As explained above, I tried to achieve this order from my client code, but faced some hard limitations

Additional context If the proposed approach looks good to you, and if it could help, I guess I could try to propose a pull request to implement this.

gturri commented 1 year ago

FWIW (in case someone has the same issue and stumble on this feature request), I found a workaround, which is in a nutshell:

From a code point of view it looks like this:

var oas = ...

// Ensure the schemas are sorted. This can be done directly on the OpenApiDocument
oas.Components.Schemas = new SortedList<string, OpenApiSchema>(oas.Components.Schemas, StringComparer.OrdinalIgnoreCase);

// Now the trick to sort the paths
using var oasWriter = new StringWriter();
oas.SerializeAsV3(new OpenApiYamlWriter(oasWriter));
var yamlDoc = (Dictionary<object, object>) new YamlDotNet.Serialization.Deserializer().Deserialize(new StringReader(oasWriter.ToString()));
yamlDoc["paths"] = ToDictionaryWithSortedKeyStrings((Dictionary<object, object>) yamlDoc["paths"]);

// Now I can serialize back (with YamlDotNet this time)
using var textWriter = new StreamWriter(outPath);
new YamlDotNet.Serialization.Serializer().Serialize(textWriter, yamlDoc);

// A trick here is that we get a IDictionary<object, object> but we know that the keys are string, and we need to
// cast them in order to apply a deterministic order to it (because if I don't use StringComparer.OrdinalIgnoreCase
// I may end up with a different behavior on my laptop and on my CI
private static SortedList<string, object> ToDictionaryWithSortedKeyStrings(IDictionary<object, object> dictionary)
{
    var sorted = new SortedList<string, object>(StringComparer.OrdinalIgnoreCase);
    foreach (var kvp in dictionary)
    {
        sorted.Add(kvp.Key.ToString(), kvp.Value);
    }
    return sorted;
}

That being said, I guess it makes sense to leave this feature request open, because it would be much more convenient if this could be supported directly by OpenAPI.NET