kiota icon indicating copy to clipboard operation
kiota copied to clipboard

Runtime support of union types is limited to TypeScript clients

Open darrelmiller opened this issue 3 years ago • 6 comments

Due to lack of support for union types in other languages, we do not have an easy way to map in runtime response to the appropriate property on the wrapper created for union types. Support for this would require taking a dependency on a JSON Schema validation library to determine which of the schemas map to the response.

Users can always decorate their API with a discriminator for non-scalar types.

Further investigation is required.

darrelmiller avatar Mar 18 '22 16:03 darrelmiller

For union and exclusion types that are non-scalar, a discriminator is required to enable runtime differentiation between the returned type.

MyResponseType:
  oneOf:
  - $ref: '#/components/schemas/Cat'
  - $ref: '#/components/schemas/Dog'
  - $ref: '#/components/schemas/Lizard'
  - $ref: 'https://gigantic-server.com/schemas/Monster/schema.json'
  discriminator:
    propertyName: petType
    mapping:
      dog: '#/components/schemas/Dog'
      monster: 'https://gigantic-server.com/schemas/Monster/schema.json'

For scalar types, we would brute force match the returned value against the schema types listed in the oneOf until we find a match.

If a oneOf is present but there is no discriminator, look into the schemas in the oneOf for the discriminator. By convention when we generate the OpenAPI from Graph, we put the base type as the first schema in the oneOf and it contains the discriminator. Only use the mappings in from the discriminator that are used by the oneOf.

darrelmiller avatar May 20 '22 16:05 darrelmiller

in case we have one of more complex types in the oneOf/anyOf but are not able to find discriminator information, an error should be logged but the generation process should continue.

baywet avatar Jun 02 '22 14:06 baywet

If possible we should prevent the RequestFactory from being emitted, but probably still emit the model or we could cause cascading errors.

darrelmiller avatar Jun 02 '22 14:06 darrelmiller

From our most recent design session:

AllOfs in schemas included in an anyOf cause an issue if we merge anyOfs into a single wrapper.

Union type maps to oneOf Intersection type maps to anyOf

To support anyOf we have two choices: A) Merge all the properties of the child schemas into the wrapper type B) Create properties in the wrapper type for each of the child schemas

Option A doesn't work cleanly for child schemas that are scalar types and it doesn't work for child schemas that use allOf for inheritance.

Option B has a problem where properties exist in multiple child schemas. We are preferring Option B and telling customers that if a property exists in multiple child schemas then we will only deserialize to one of the properties.

This means that oneOf and anyOf wrappers are the same shape.

oneOfs should come with a discriminator to know which property to deserialize the object into.

We think we need the additionalProperties dictionary on the wrapper class.

If a oneOf has a child schema of type object, then a discriminator is required.

https://spec.openapis.org/registry/format/ Formats Registry Extensible data value repository

public class ComposedWrapper : IUnionType (oneOf) | IIntersectionType (anyOf) {
	public ComposedType1 ComposedType1 { get; set; }
	public ComposedType2 ComposedType2 { get; set; }
	public string StringResult { get; set; }
	public string WhichFieldInfo {get; set;} // only for union type

	public static ComposedWrapper CreateFromDiscriminator(IParseNode parseNode) {
		var result = new ComposedWrapper();
		var discriminator = parseNode.GetDiscriminator();

		// when any of
		result.composedType1 = new();
		result.composedType2 = new();
		whichFieldInfo = "composedType1,composedType1;composedType2,composedType1;";
		// when one of
		if("#microsoft.graph.composedType1".Equals(discriminator)) {
			result.composedType1 = new();
			whichFieldInfo = "composedType1,composedType1"; // (symbol name, serialization name)
		}
		if("#microsoft.graph.composedType2".Equals(discriminator)) {
			result.composedType2 = new();
			whichFieldInfo = "composedType2,composedType2";
		}
		// end when
		if(parseNode.GetStringValue() is string stringValue) {
			result.StringResult = stringValue;
			whichFieldInfo = "noop"; // only for union types/one of
		}
		return result;
	}

	public Dictionary<string, Action<ParseNode>> GetFieldDeserializers() {
		return new Dictionary<string, Action<ParseNode>> {
			{ "composedType1", (node) => {
				this.composedType1 = node.GetObjectValue<ComposedType1>();
			} },
			{ "composedType2", (node) => {
				this.composedType1 = node.GetObjectValue<ComposedType2>();
			} },
			{ "string", (node) => {
				this.string = node.GetString();
			} },
		};
	}
}

public class JsonParseNode {
	public T GetObjectValue<T>(Func<T> factory) {
		var rawJsonValue = new ();
		var instance = factory(this);
		if(instance is IIntersectionType) {
			var fieldDeserializers = instance.ComposedType1.GetFieldDeserializers().Union(instance.ComposedType1.GetFieldDeserializers()); // AnyOf get the properties by reflection using the whichFieldInfo
			// use a multi-map to get the field deserializers in case fields with the same name are defined on different members
		} else if(instance is IUnionType) {
			var fieldDeserializers = instance.ComposedType1.GetFieldDeserializers(); // one of (get the right property by reflection using the whichFieldInfo)
		} else {
			var fieldDeserializers = instance.GetFieldDeserializers(); // regular object and all of
		}

		foreach(var field in rawJsonValue) {
			var fieldDeserializer = fieldDeserializers[field.Key];
			fieldDeserializer(field.Value);
		}
		// additional data
		return JsonConvert.DeserializeObject<T>(this.Value);
	}
}

baywet avatar Jun 30 '22 16:06 baywet

Integration tests:

  • oneOf https://github.com/APIs-guru/openapi-directory/blob/main/APIs/nexmo.com/verify/1.2.0/openapi.yaml
  • anyOf https://github.com/APIs-guru/openapi-directory/blob/b4e13961fbab0f63a840d2e273acba1d2020fa04/APIs/apideck.com/vault/8.34.0/openapi.yaml#L1721

baywet avatar Jul 11 '22 19:07 baywet

additional testing sets https://github.com/APIs-guru/openapi-directory/blob/main/APIs/influxdata.com/2.0.0/openapi.yaml https://github.com/APIs-guru/openapi-directory/blob/main/APIs/exavault.com/2.0/openapi.yaml https://github.com/APIs-guru/openapi-directory/blob/main/APIs/twitter.com/current/2.21/openapi.yaml https://github.com/APIs-guru/openapi-directory/blob/main/APIs/cpy.re/peertube/3.3.0/openapi.yaml https://github.com/APIs-guru/openapi-directory/blob/main/APIs/flat.io/2.13.0/openapi.yaml

baywet avatar Aug 12 '22 19:08 baywet