// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Text.Json; namespace EpicGames.Core { /// /// Interface used to write JSON schema types. This is abstracted to allow multiple passes over the document structure, in order to optimize multiple references to the same type definition. /// public interface IJsonSchemaWriter { /// void WriteStartObject(); /// void WriteStartObject(string name); /// void WriteEndObject(); /// void WriteStartArray(); /// void WriteStartArray(string name); /// void WriteEndArray(); /// void WriteBoolean(string name, bool value); /// void WriteString(string key, string value); /// void WriteStringValue(string name); /// /// Serialize a type to JSON /// /// void WriteType(JsonSchemaType type); } /// /// Implementation of a JSON schema. Implements draft 04 (latest supported by Visual Studio 2019). /// public class JsonSchema { class JsonTypeRefCollector : IJsonSchemaWriter { /// /// Reference counts for each type (max of 2) /// public Dictionary TypeRefCount { get; } = []; /// public void WriteBoolean(string name, bool value) { } /// public void WriteString(string key, string value) { } /// public void WriteStringValue(string name) { } /// public void WriteStartObject() { } /// public void WriteStartObject(string name) { } /// public void WriteEndObject() { } /// public void WriteStartArray() { } /// public void WriteStartArray(string name) { } /// public void WriteEndArray() { } /// public void WriteType(JsonSchemaType type) { if (type is not JsonSchemaPrimitiveType) { TypeRefCount.TryGetValue(type, out int refCount); if (refCount < 2) { TypeRefCount[type] = ++refCount; } if (refCount < 2) { type.Write(this); } } } } /// /// Implementation of /// class JsonSchemaWriter : IJsonSchemaWriter { /// /// Raw Json output /// readonly Utf8JsonWriter _jsonWriter; /// /// Mapping of type to definition name /// readonly Dictionary _typeToDefinition; /// /// Constructor /// /// /// public JsonSchemaWriter(Utf8JsonWriter writer, Dictionary typeToDefinition) { _jsonWriter = writer; _typeToDefinition = typeToDefinition; } /// public void WriteBoolean(string name, bool value) => _jsonWriter.WriteBoolean(name, value); /// public void WriteString(string key, string value) => _jsonWriter.WriteString(key, value); /// public void WriteStringValue(string name) => _jsonWriter.WriteStringValue(name); /// public void WriteStartObject() => _jsonWriter.WriteStartObject(); /// public void WriteStartObject(string name) => _jsonWriter.WriteStartObject(name); /// public void WriteEndObject() => _jsonWriter.WriteEndObject(); /// public void WriteStartArray() => _jsonWriter.WriteStartArray(); /// public void WriteStartArray(string name) => _jsonWriter.WriteStartArray(name); /// public void WriteEndArray() => _jsonWriter.WriteEndArray(); /// /// Writes a type, either inline or as a reference to a definition elsewhere /// /// public void WriteType(JsonSchemaType type) { if (_typeToDefinition.TryGetValue(type, out string? definition)) { _jsonWriter.WriteString("$ref", $"#/definitions/{definition}"); } else { type.Write(this); } } } /// /// Identifier for the schema /// public string? Id { get; set; } /// /// The root schema type /// public JsonSchemaType RootType { get; set; } /// /// Constructor /// /// Id for the schema /// public JsonSchema(string? id, JsonSchemaType rootType) { Id = id; RootType = rootType; } /// /// Write this schema to a byte array /// /// public void Write(Utf8JsonWriter writer) { // Determine reference counts for each type. Any type referenced at least twice will be split off into a separate definition. JsonTypeRefCollector refCollector = new JsonTypeRefCollector(); refCollector.WriteType(RootType); // Assign names to each type definition HashSet definitionNames = []; Dictionary typeToDefinition = []; foreach ((JsonSchemaType type, int refCount) in refCollector.TypeRefCount) { if (refCount > 1) { string baseName = type.Name ?? "unnamed"; string name = baseName; for (int idx = 1; !definitionNames.Add(name); idx++) { name = $"{baseName}{idx}"; } typeToDefinition[type] = name; } } // Write the schema writer.WriteStartObject(); writer.WriteString("$schema", "http://json-schema.org/draft-04/schema#"); if (Id != null) { writer.WriteString("$id", Id); } JsonSchemaWriter schemaWriter = new JsonSchemaWriter(writer, typeToDefinition); RootType.Write(schemaWriter); if (typeToDefinition.Count > 0) { writer.WriteStartObject("definitions"); foreach ((JsonSchemaType type, string refName) in typeToDefinition) { writer.WriteStartObject(refName); type.Write(schemaWriter); writer.WriteEndObject(); } writer.WriteEndObject(); } writer.WriteEndObject(); } /// /// Write this schema to a stream /// /// The output stream public void Write(Stream stream) { using (Utf8JsonWriter writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true })) { Write(writer); } } /// /// Writes this schema to a file /// /// The output file public void Write(FileReference file) { using (FileStream stream = FileReference.Open(file, FileMode.Create)) { Write(stream); } } /// /// Constructs a Json schema from a type /// /// The type to construct from /// Reads documentation for requested types /// New schema object public static JsonSchema FromType(Type type, XmlDocReader? xmlDocReader) { JsonSchemaFactory factory = new JsonSchemaFactory(xmlDocReader); return factory.CreateSchema(type); } /// /// Create a Json schema (or retrieve a cached schema) /// public static JsonSchema CreateSchema(Type type, XmlDocReader? xmlDocReader) { JsonSchemaFactory factory = new JsonSchemaFactory(xmlDocReader); return factory.CreateSchema(type); } } }