Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3
Schemas
AsyncAPI.Net supports 3 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.
newAsyncApiJsonSchema(){Title="title1",AllOf=newList<AsyncApiJsonSchema>{newAsyncApiJsonSchema{Title="title2",Properties=newDictionary<string,AsyncApiJsonSchema>{["property1"]=newAsyncApiJsonSchema{Type=SchemaType.Integer,},["property2"]=newAsyncApiJsonSchema{Type=SchemaType.String,MaxLength=15,},},},newAsyncApiJsonSchema{Title="title3",Properties=newDictionary<string,AsyncApiJsonSchema>{["property3"]=newAsyncApiJsonSchema{Properties=newDictionary<string,AsyncApiJsonSchema>{["property4"]=newAsyncApiJsonSchema{Type=SchemaType.Boolean,},},},["property5"]=newAsyncApiJsonSchema{Type=SchemaType.String,MinLength=2,},},Nullable=true,},},Nullable=true,ExternalDocs=newAsyncApiExternalDocumentation{Url=newUri("http://example.com/externalDocs"),},};Due to the nature of 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
- Named type
Note, all above types are prefixed "Avro" within the dotnet classes.
Avro named schemas can be referenced by name using AvroNamedType. This is separate from AsyncAPI $ref; Avro named references serialize as plain strings such as "LongList", not as $ref objects.
Named types are resolved using the Avro name and namespace rules. A named schema must be defined before it is referenced, following Avro's depth-first, left-to-right traversal rule. If a name cannot be resolved, validation reports an AsyncApiValidatorWarning instead of a validation error.
Named references can also be used recursively and inside schemas such as arrays, unions, and map values.
newAvroRecord{Name="LongList",Fields=newList<AvroField>{newAvroField{Name="value",Type=AvroPrimitiveType.Long,},newAvroField{Name="next",Type=newAvroUnion{Types=newList<AsyncApiAvroSchema>{AvroPrimitiveType.Null,newAvroNamedType{Name="LongList",},},},},},};AvroMap.Values accepts any AsyncApiAvroSchema, not only primitive types, so maps can use named schemas as values.
newAvroMap{Values=newAvroNamedType{Name="Address",},};newAvroRecord{Name="User",Namespace="com.example",Fields=newList<AvroField>{newAvroField{Name="username",Type=AvroPrimitiveType.String,Doc="The username of the user.",Default=newAsyncApiAny("guest"),Order=AvroFieldOrder.Ascending,},newAvroField{Name="status",Type=newAvroEnum{Name="Status",Symbols=newList<string>{"ACTIVE","INACTIVE","BANNED"},},Doc="The status of the user.",},newAvroField{Name="emails",Type=newAvroArray{Items=AvroPrimitiveType.String,},Doc="A list of email addresses.",},newAvroField{Name="metadata",Type=newAvroMap{Values=AvroPrimitiveType.String,},Doc="Metadata associated with the user.",},newAvroField{Name="address",Type=newAvroRecord{Name="Address",Fields=newList<AvroField>{newAvroField{Name="street",Type=AvroPrimitiveType.String},newAvroField{Name="city",Type=AvroPrimitiveType.String},newAvroField{Name="zipcode",Type=AvroPrimitiveType.String},},},Doc="The address of the user.",},newAvroField{Name="profilePicture",Type=newAvroFixed{Name="ProfilePicture",Size=256,},Doc="A fixed-size profile picture.",},newAvroField{Name="contact",Type=newAvroUnion{Types=newList<AsyncApiAvroSchema>{AvroPrimitiveType.Null,newAvroRecord{Name="PhoneNumber",Fields=newList<AvroField>{newAvroField{Name="countryCode",Type=AvroPrimitiveType.Int},newAvroField{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.
varsettings=newAsyncApiReaderSettings();settings.SchemaParserRegistry.RegisterParser(newCustomParser());varreader=newAsyncApiStringReader(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 don't have to implement the SerializeV methods.
The typed nature of AsyncAPI.NET is what sets it apart, so of course you need to implement a model to hold the values.
The following is an excerpt from the JSONSchema implementation.
publicclassAsyncApiJsonSchema:IAsyncApiSchema{publicstringTitle{get;set;}publicSchemaType?Type{get;set;}publicISet<string>Required{get;set;}=newHashSet<string>();publicdouble?Maximum{get;set;}publicIList<AsyncApiJsonSchema>AllOf{get;set;}=newList<AsyncApiJsonSchema>();publicAsyncApiJsonSchemaIf{get;set;}publicIDictionary<string,AsyncApiJsonSchema>Properties{get;set;}=newDictionary<string,AsyncApiJsonSchema>();publicIList<AsyncApiAny>Enum{get;set;}=newList<AsyncApiAny>();publicAsyncApiAnyConst{get;set;}publicboolNullable{get;set;}publicvoidSerializeV2(IAsyncApiWriterwriter){this.SerializeCore(writer);}publicvoidSerializeV3(IAsyncApiWriterwriter){this.SerializeCore(writer);}privatevoidSerializeCore(IAsyncApiWriterwriter){writer.WriteStartObject();// titlewriter.WriteOptionalProperty(AsyncApiConstants.Title,this.Title);// typeif(this.Type!=null){vartypes=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));}}// maximumwriter.WriteOptionalProperty(AsyncApiConstants.Maximum,this.Maximum);// allOfwriter.WriteOptionalCollection(AsyncApiConstants.AllOf,this.AllOf,(w,s)=>s.SerializeV2(w));// uniqueItemswriter.WriteOptionalProperty(AsyncApiConstants.UniqueItems,this.UniqueItems);// propertieswriter.WriteOptionalMap(AsyncApiConstants.Properties,this.Properties,(w,s)=>s.SerializeV2(w));// enumwriter.WriteOptionalCollection(AsyncApiConstants.Enum,this.Enum,(nodeWriter,s)=>nodeWriter.WriteAny(s));writer.WriteOptionalObject(AsyncApiConstants.Const,this.Const,(w,s)=>w.WriteAny(s));// nullablewriter.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).
privatestaticreadonlyFixedFieldMap<AsyncApiJsonSchema>schemaFixedFields=new(){{"title",(a,n)=>{a.Title=n.GetScalarValue();}},{"type",(a,n)=>{a.Type=n.GetScalarValue().GetEnumFromDisplayName<SchemaType>();}},{"required",(a,n)=>{a.Required=newHashSet<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 action that takes the ParseNode and creates the proper structure depending on the type of property. There are 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:
publicIAsyncApiSchemaLoadSchema(ParseNodenode){// Check that we are dealing with an object and cast accordingly.varmapNode=node.CheckMapNode("arbitrary string");varschema=newMyCustomSchema();// map each propery against the fixedFieldMapforeach(varpropertyinmapNode){property.ParseField(schema,schemaFixedFields,null);}returnschema;}The latter should simply be the list of formats that should resolve to this schema. For JsonSchema its looks like this:
publicIEnumerable<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