diff --git a/packages/quicktype-core/src/UnifyClasses.ts b/packages/quicktype-core/src/UnifyClasses.ts index b6a5a6efb..9fbca9b92 100644 --- a/packages/quicktype-core/src/UnifyClasses.ts +++ b/packages/quicktype-core/src/UnifyClasses.ts @@ -26,6 +26,7 @@ function getCliqueProperties( clique: ObjectType[], builder: TypeBuilder, makePropertyType: (types: ReadonlySet) => TypeRef, + preserveSchemaRequiredProperties: boolean, ): [ReadonlyMap, TypeRef | undefined, boolean] { const lostTypeAttributes = false; const propertyNames = new Set(); @@ -53,7 +54,15 @@ function getCliqueProperties( let [name, types, isOptional] = property; const maybeProperty = o.getProperties().get(name); if (maybeProperty === undefined) { - isOptional = true; + // An open alternative can still contain this property as + // optional additional data. + if ( + !preserveSchemaRequiredProperties || + additional !== undefined + ) { + isOptional = true; + } + if (additional !== undefined && additional.kind !== "any") { types.add(additional); } @@ -126,6 +135,7 @@ export class UnifyUnionBuilder extends UnionBuilder< private readonly _makeObjectTypes: boolean, private readonly _makeClassesFixed: boolean, private readonly _unifyTypes: (typesToUnify: TypeRef[]) => TypeRef, + private readonly _preserveSchemaRequiredProperties = false, ) { super(typeBuilder); } @@ -193,12 +203,17 @@ export class UnifyUnionBuilder extends UnionBuilder< ); } else { const [properties, additionalProperties, lostTypeAttributes] = - getCliqueProperties(objectTypes, this.typeBuilder, (types) => { - assert(types.size > 0, "Property has no type"); - return this._unifyTypes( - Array.from(types).map((t) => t.typeRef), - ); - }); + getCliqueProperties( + objectTypes, + this.typeBuilder, + (types) => { + assert(types.size > 0, "Property has no type"); + return this._unifyTypes( + Array.from(types).map((t) => t.typeRef), + ); + }, + this._preserveSchemaRequiredProperties, + ); if (lostTypeAttributes) { this.typeBuilder.setLostTypeAttributes(); } diff --git a/packages/quicktype-core/src/attributes/Schema.ts b/packages/quicktype-core/src/attributes/Schema.ts new file mode 100644 index 000000000..dfc24209c --- /dev/null +++ b/packages/quicktype-core/src/attributes/Schema.ts @@ -0,0 +1,18 @@ +import { TypeAttributeKind } from "./TypeAttributes.js"; + +class SchemaSetOperationTypeAttributeKind extends TypeAttributeKind { + public constructor() { + super("schemaSetOperation"); + } + + public combine(_: true[]): true { + return true; + } + + public makeInferred(_: true): true { + return true; + } +} + +export const schemaSetOperationTypeAttributeKind: TypeAttributeKind = + new SchemaSetOperationTypeAttributeKind(); diff --git a/packages/quicktype-core/src/input/JSONSchemaInput.ts b/packages/quicktype-core/src/input/JSONSchemaInput.ts index 66b1c4104..a9ed84fec 100644 --- a/packages/quicktype-core/src/input/JSONSchemaInput.ts +++ b/packages/quicktype-core/src/input/JSONSchemaInput.ts @@ -33,6 +33,7 @@ import { defaultValueAttributeProducer } from "../attributes/DefaultValue.js"; import { descriptionAttributeProducer } from "../attributes/Description.js"; import { enumValuesAttributeProducer } from "../attributes/EnumValues.js"; import { StringTypes } from "../attributes/StringTypes.js"; +import { schemaSetOperationTypeAttributeKind } from "../attributes/Schema.js"; import { type TypeAttributes, combineTypeAttributes, @@ -1251,6 +1252,11 @@ async function addTypesInSchema( ); } + unionAttributes = combineTypeAttributes( + "union", + unionAttributes, + schemaSetOperationTypeAttributeKind.makeAttributes(true), + ); const unionType = typeBuilder.getUniqueUnionType( unionAttributes, undefined, diff --git a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts index f87128473..94dfd8e0f 100644 --- a/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts +++ b/packages/quicktype-core/src/language/CPlusPlus/CPlusPlusRenderer.ts @@ -777,8 +777,18 @@ export class CPlusPlusRenderer extends ConvenienceRenderer { this.variantIndirection( classType, ctx.needsForwardIndirection && - this.isForwardDeclaredType(classType) && - !isOptional, + !isOptional && + // A required class-typed member still needs heap + // indirection when the class participates in a cycle, + // otherwise it would be stored by value and form an + // incomplete recursive type. Optional members already + // get this through `isCycleBreakerType` in + // `isOptionalAsValuePossible`; mirror that here so that + // required recursive members (e.g. a `oneOf`/`anyOf` + // branch that requires a self-referential property) + // compile. + (this.isForwardDeclaredType(classType) || + this.isCycleBreakerType(classType)), [ this.ourQualifier(inJsonNamespace), this.nameForNamedType(classType), diff --git a/packages/quicktype-core/src/rewrites/FlattenUnions.ts b/packages/quicktype-core/src/rewrites/FlattenUnions.ts index aab4d8511..559d1442c 100644 --- a/packages/quicktype-core/src/rewrites/FlattenUnions.ts +++ b/packages/quicktype-core/src/rewrites/FlattenUnions.ts @@ -1,5 +1,6 @@ import { iterableSome, setFilter } from "collection-utils"; +import { schemaSetOperationTypeAttributeKind } from "../attributes/Schema.js"; import { emptyTypeAttributes } from "../attributes/TypeAttributes.js"; import type { GraphRewriteBuilder } from "../GraphRewriting.js"; import { messageAssert } from "../Messages.js"; @@ -136,11 +137,19 @@ export function flattenUnions( ); }; + const isSchemaSetOperation = iterableSome( + types, + (t) => + schemaSetOperationTypeAttributeKind.tryGetInAttributes( + t.getAttributes(), + ) !== undefined, + ); unionBuilder = new UnifyUnionBuilder( builder, makeObjectTypes, true, unifyTypeRefs, + isSchemaSetOperation, ); return unifyTypes( types, diff --git a/test/inputs/schema/required-in-any-of.1.json b/test/inputs/schema/required-in-any-of.1.json new file mode 100644 index 000000000..8075931d8 --- /dev/null +++ b/test/inputs/schema/required-in-any-of.1.json @@ -0,0 +1,5 @@ +{ + "anyof": { + "name": "example" + } +} diff --git a/test/inputs/schema/required-in-any-of.2.fail.strict-optional.json b/test/inputs/schema/required-in-any-of.2.fail.strict-optional.json new file mode 100644 index 000000000..1f3ac8a9b --- /dev/null +++ b/test/inputs/schema/required-in-any-of.2.fail.strict-optional.json @@ -0,0 +1,3 @@ +{ + "anyof": {} +} diff --git a/test/inputs/schema/required-in-any-of.schema b/test/inputs/schema/required-in-any-of.schema new file mode 100644 index 000000000..02c18d182 --- /dev/null +++ b/test/inputs/schema/required-in-any-of.schema @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "properties": { + "anyof": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "size": { "type": "string" } + }, + "required": [], + "minProperties": 1, + "additionalProperties": false + } + ] + } + }, + "required": ["anyof"], + "additionalProperties": false +} diff --git a/test/languages.ts b/test/languages.ts index 120f3170b..3f5423b6b 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -2273,6 +2273,7 @@ export const ElixirLanguage: Language = { // Struct keys cannot be enforced at runtime in Elixir and their values will just be set to null. "strict-optional.schema", "required.schema", + "required-in-any-of.schema", // The default-value fail sample also relies on required-property enforcement. "default-value.schema", "boolean-subschema.schema",