Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions packages/quicktype-core/src/UnifyClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function getCliqueProperties(
clique: ObjectType[],
builder: TypeBuilder,
makePropertyType: (types: ReadonlySet<Type>) => TypeRef,
preserveSchemaRequiredProperties: boolean,
): [ReadonlyMap<string, ClassProperty>, TypeRef | undefined, boolean] {
const lostTypeAttributes = false;
const propertyNames = new Set<string>();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
}
Expand Down
18 changes: 18 additions & 0 deletions packages/quicktype-core/src/attributes/Schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TypeAttributeKind } from "./TypeAttributes.js";

class SchemaSetOperationTypeAttributeKind extends TypeAttributeKind<true> {
public constructor() {
super("schemaSetOperation");
}

public combine(_: true[]): true {
return true;
}

public makeInferred(_: true): true {
return true;
}
}

export const schemaSetOperationTypeAttributeKind: TypeAttributeKind<true> =
new SchemaSetOperationTypeAttributeKind();
6 changes: 6 additions & 0 deletions packages/quicktype-core/src/input/JSONSchemaInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1251,6 +1252,11 @@ async function addTypesInSchema(
);
}

unionAttributes = combineTypeAttributes(
"union",
unionAttributes,
schemaSetOperationTypeAttributeKind.makeAttributes(true),
);
const unionType = typeBuilder.getUniqueUnionType(
unionAttributes,
undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
9 changes: 9 additions & 0 deletions packages/quicktype-core/src/rewrites/FlattenUnions.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions test/inputs/schema/required-in-any-of.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"anyof": {
"name": "example"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"anyof": {}
}
29 changes: 29 additions & 0 deletions test/inputs/schema/required-in-any-of.schema
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading