-
-
Notifications
You must be signed in to change notification settings - Fork 0
Schemas
AsyncAPI.Net supports 2 types of message payloads
- Schema Object
- Avro 1.9.0
- Custom formats.
The payload types are AsyncApiJsonSchema
and AsyncApiAvroSchema
respectively.
note that AsyncApiJsonSchema
is implicitly convertable to AsyncMultiFormatSchema
.
new AsyncApiJsonSchema()
{
Title = "title1",
AllOf = new List<AsyncApiJsonSchema>
{
new AsyncApiJsonSchema
{
Title = "title2",
Properties = new Dictionary<string, AsyncApiJsonSchema>
{
["property1"] = new AsyncApiJsonSchema
{
Type = SchemaType.Integer,
},
["property2"] = new AsyncApiJsonSchema
{
Type = SchemaType.String,
MaxLength = 15,
},
},
},
new AsyncApiJsonSchema
{
Title = "title3",
Properties = new Dictionary<string, AsyncApiJsonSchema>
{
["property3"] = new AsyncApiJsonSchema
{
Properties = new Dictionary<string, AsyncApiJsonSchema>
{
["property4"] = new AsyncApiJsonSchema
{
Type = SchemaType.Boolean ,
},
},
},
["property5"] = new AsyncApiJsonSchema
{
Type = SchemaType.String,
MinLength = 2,
},
},
Nullable = true,
},
},
Nullable = true,
ExternalDocs = new AsyncApiExternalDocumentation
{
Url = new Uri("http://example.com/externalDocs"),
},
};
Due to the nature of the avro, the payload type has a helper method bool TryGetAs<T>(out T schema)
to make the casting logic slightly easier on you.
The Avro types are implemented through a common base class AsyncApiAvroSchema
As avro supports adding custom properties to the schemas as "metadata", these when deserialized, will be added to the Metadata
Dictionary which exists on all implemented Avro types
Supported types:
- Record
- Fixed
- Enum
- Union
- Map
- Array
- Primitive
- Field
Note, all above types are prefixed "Avro" within the dotnet classes.
new AvroRecord
{
Name = "User",
Namespace = "com.example",
Fields = new List<AvroField>
{
new AvroField
{
Name = "username",
Type = AvroPrimitiveType.String,
Doc = "The username of the user.",
Default = new AsyncApiAny("guest"),
Order = AvroFieldOrder.Ascending,
},
new AvroField
{
Name = "status",
Type = new AvroEnum
{
Name = "Status",
Symbols = new List<string> { "ACTIVE", "INACTIVE", "BANNED" },
},
Doc = "The status of the user.",
},
new AvroField
{
Name = "emails",
Type = new AvroArray
{
Items = AvroPrimitiveType.String,
},
Doc = "A list of email addresses.",
},
new AvroField
{
Name = "metadata",
Type = new AvroMap
{
Values = AvroPrimitiveType.String,
},
Doc = "Metadata associated with the user.",
},
new AvroField
{
Name = "address",
Type = new AvroRecord
{
Name = "Address",
Fields = new List<AvroField>
{
new AvroField { Name = "street", Type = AvroPrimitiveType.String },
new AvroField { Name = "city", Type = AvroPrimitiveType.String },
new AvroField { Name = "zipcode", Type = AvroPrimitiveType.String },
},
},
Doc = "The address of the user.",
},
new AvroField
{
Name = "profilePicture",
Type = new AvroFixed
{
Name = "ProfilePicture",
Size = 256,
},
Doc = "A fixed-size profile picture.",
},
new AvroField
{
Name = "contact",
Type = new AvroUnion
{
Types = new List<AvroSchema>
{
AvroPrimitiveType.Null,
new AvroRecord
{
Name = "PhoneNumber",
Fields = new List<AvroField>
{
new AvroField { Name = "countryCode", Type = AvroPrimitiveType.Int },
new AvroField { Name = "number", Type = AvroPrimitiveType.String },
},
},
},
},
Doc = "The contact information of the user, which can be either null or a phone number.",
},
},
};
You can define custom payloads/formats by implementing ISchemaParser
and attaching it via to the readers settings
var settings = new AsyncApiReaderSettings();
settings.SchemaParserRegistry.RegisterParser(new CustomParser());
var reader = new AsyncApiStringReader(settings);
There are a few moving parts that needs to align. Mainly you will need a model to hold the values and the parser itself.
Note: If you don't need to serialize the model (meaning only read, and not write it), you dont have to implement the SerializeV
methods.
The typed nature of AsyncAPI.NET us what sets it apart, so ofcourse you need to implement a model to hold the values.
The following is an excerpt from the JSONSchema implementation.
public class AsyncApiJsonSchema : IAsyncApiSchema
{
public string Title { get; set; }
public SchemaType? Type { get; set; }
public ISet<string> Required { get; set; } = new HashSet<string>();
public double? Maximum { get; set; }
public IList<AsyncApiJsonSchema> AllOf { get; set; } = new List<AsyncApiJsonSchema>();
public AsyncApiJsonSchema If { get; set; }
public IDictionary<string, AsyncApiJsonSchema> Properties { get; set; } = new Dictionary<string, AsyncApiJsonSchema>();
public IList<AsyncApiAny> Enum { get; set; } = new List<AsyncApiAny>();
public AsyncApiAny Const { get; set; }
public bool Nullable { get; set; }
public void SerializeV2(IAsyncApiWriter writer)
{
this.SerializeCore(writer);
}
public void SerializeV3(IAsyncApiWriter writer)
{
this.SerializeCore(writer);
}
private void SerializeCore(IAsyncApiWriter writer)
{
writer.WriteStartObject();
// title
writer.WriteOptionalProperty(AsyncApiConstants.Title, this.Title);
// type
if (this.Type != null)
{
var types = EnumExtensions.GetFlags<SchemaType>(this.Type.Value);
if (types.Count() == 1)
{
writer.WriteOptionalProperty(AsyncApiConstants.Type, types.First().GetDisplayName());
}
else
{
writer.WriteOptionalCollection(AsyncApiConstants.Type, types.Select(t => t.GetDisplayName()), (w, s) => w.WriteValue(s));
}
}
// maximum
writer.WriteOptionalProperty(AsyncApiConstants.Maximum, this.Maximum);
// allOf
writer.WriteOptionalCollection(AsyncApiConstants.AllOf, this.AllOf, (w, s) => s.SerializeV2(w));
// uniqueItems
writer.WriteOptionalProperty(AsyncApiConstants.UniqueItems, this.UniqueItems);
// properties
writer.WriteOptionalMap(AsyncApiConstants.Properties, this.Properties, (w, s) => s.SerializeV2(w));
// enum
writer.WriteOptionalCollection(AsyncApiConstants.Enum, this.Enum, (nodeWriter, s) => nodeWriter.WriteAny(s));
writer.WriteOptionalObject(AsyncApiConstants.Const, this.Const, (w, s) => w.WriteAny(s));
// nullable
writer.WriteOptionalProperty(AsyncApiConstants.Nullable, this.Nullable, false);
writer.WriteEndObject();
}
}
So basically. Define the properties. Define how they are serialized.
Deserializers/Parsers in AsyncAPI.NET uses maps of fields that takes an Action<T>
that tells it how to get the proper value out.
You can see an example of this below (excerpt from the JsonSchemaDeserializer).
private static readonly FixedFieldMap<AsyncApiJsonSchema> schemaFixedFields = new()
{
{
"title", (a, n) => { a.Title = n.GetScalarValue(); }
},
{
"type", (a, n) => { a.Type = n.GetScalarValue().GetEnumFromDisplayName<SchemaType>(); }
},
{
"required",
(a, n) => { a.Required = new HashSet<string>(n.CreateSimpleList(n2 => n2.GetScalarValue())); }
},
{
"maximum",
(a, n) =>
{
a.Maximum = double.Parse(n.GetScalarValue(), NumberStyles.Float, n.Context.Settings.CultureInfo);
}
},
{
"uniqueItems", (a, n) => { a.UniqueItems = bool.Parse(n.GetScalarValue()); }
},
{
"enum", (a, n) => { a.Enum = n.CreateListOfAny(); }
},
{
"const", (a, n) => { a.Const = n.CreateAny(); }
},
{
"if", (a, n) => { a.If = LoadSchema(n); }
},
{
"properties", (a, n) => { a.Properties = n.CreateMap(LoadSchema); }
},
{
"allOf", (a, n) => { a.AllOf = n.CreateList(LoadSchema); }
},
{
"nullable", (a, n) => { a.Nullable = n.GetBooleanValue(); }
},
};
You'll notice its all just field names from the schema and an Acton that takes the ParseNode and creates the proper structure depending on the type of property. There a extensions for maps, collections, scalars etc.
The ISchemaParser
defines 2 methods.
IAsyncApiSchema LoadSchema(ParseNode node)
IEnumerable<string> SupportedFormats
The first should generally look something like this
public IAsyncApiSchema LoadSchema(ParseNode node)
{
// Check that we are dealing with an object and cast accordingly.
var mapNode = node.CheckMapNode("arbitrary string");
var schema = new MyCustomSchema();
// map each propery against the fixedFieldMap
foreach (var property in mapNode)
{
property.ParseField(schema, schemaFixedFields, null);
}
return schema;
}
and the latter should simply be the list of formats that should resolve to this schema. For JsonSchema its looks like this
public IEnumerable<string> SupportedFormats => new List<string>
{
"application/vnd.aai.asyncapi+json",
"application/vnd.aai.asyncapi+yaml",
"application/vnd.aai.asyncapi",
"application/schema+json;version=draft-07",
"application/schema+yaml;version=draft-07",
}
And that is all you need to implement a custom schema parser. You can check out a full example in the following Unit Test https://github.com/ByteBardOrg/AsyncAPI.NET/blob/vnext/test/ByteBard.AsyncAPI.Tests/Models/CustomSchema_Should.cs