From a9b4f290c3cbeff698c92da671a41c7110e60f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:02:21 +0100 Subject: [PATCH 01/10] Add scalar child list handling in JSON schema generation --- .../annot/generator/JsonSchemaGenerator.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 7b15a55f58..6822f6457e 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -287,6 +287,11 @@ private void processMCChilds(Model m, MainInfo main, ElementInfo i, AbstractSche continue; } + if (isScalarChildList(cei)) { + attachArrayItems(i, so, cei, scalarItemsSchema(cei)); + continue; + } + if (shouldGenerateFlowParserType(cei)) { processList(i, so, cei, getSchemaObjects(m, main, cei)); continue; @@ -349,6 +354,44 @@ private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSc return sos; } + private boolean isScalarChildList(ChildElementInfo cei) { + if (!cei.isList()) return false; + if (cei.getTypeDeclaration() == null) return false; + + String qn = cei.getTypeDeclaration().getQualifiedName().toString(); + + return "java.lang.String".equals(qn) + || "java.lang.Boolean".equals(qn) + || "java.lang.Integer".equals(qn) + || "java.lang.Long".equals(qn) + || "java.lang.Double".equals(qn) + || "java.lang.Float".equals(qn) + || "java.lang.Short".equals(qn) + || "java.lang.Byte".equals(qn); + } + + private AbstractSchema scalarItemsSchema(ChildElementInfo cei) { + String qn = cei.getTypeDeclaration().getQualifiedName().toString(); + + if ("java.lang.String".equals(qn)) { + return SchemaFactory.from("string").type("string"); + } + if ("java.lang.Boolean".equals(qn)) { + return SchemaFactory.from("boolean").type("boolean"); + } + if ("java.lang.Integer".equals(qn) + || "java.lang.Long".equals(qn) + || "java.lang.Short".equals(qn) + || "java.lang.Byte".equals(qn)) { + return SchemaFactory.from("integer").type("integer"); + } + if ("java.lang.Double".equals(qn) || "java.lang.Float".equals(qn)) { + return SchemaFactory.from("number").type("number"); + } + + return SchemaFactory.from("string").type("string"); + } + private boolean isComponentsList(ElementInfo parent, ChildElementInfo cei) { return COMPONENTS.equals(parent.getAnnotation().name()) && parent.getAnnotation().noEnvelope() From e41be70a927cbd19364dc649944bd9ff8f05e27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:24:19 +0100 Subject: [PATCH 02/10] Refactor JSON schema generation to utilize helper methods for $ref definitions --- .../annot/generator/JsonSchemaGenerator.java | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 6822f6457e..3797a97a3a 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -45,6 +45,9 @@ public class JsonSchemaGenerator extends AbstractGrammar { public static final String MEMBRANE_SCHEMA_JSON_FILENAME = "membrane.schema.json"; public static final String COMPONENTS = "components"; private static final String INTERCEPTOR_FQN = "com.predic8.membrane.core.interceptor.Interceptor"; + private static final String JSON_SCHEMA_DEFS_PREFIX = "#/$defs/"; + private static final String $_REF = "$ref"; + private static final String FLOW_PARSER_DEF_NAME = "flowParser"; // TODO keep this pattern or allow *? public static final String COMPONENT_ID_PATTERN = "^[A-Za-z_][A-Za-z0-9_-]*$"; @@ -85,8 +88,7 @@ private void addTopLevelProperties(Model m, MainInfo main) { for (ElementInfo e : top) { String name = e.getAnnotation().name(); - String refName = "#/$defs/" + e.getXSDTypeName(m); - schema.property(ref(name).ref(refName)); + schema.property(defsSchemaRef(name, e.getXSDTypeName(m))); } if (!top.isEmpty()) { @@ -129,9 +131,9 @@ private AbstractSchema createParser(Model m, MainInfo main, ElementInfo eleme } if (shouldGenerateFlowParserType(child)) { - return ref(parserName).ref("#/$defs/flowParser"); + return ref(parserName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); } - return ref(parserName).ref("#/$defs/%sParser".formatted(childName)); + return ref(parserName).ref(defsRefPath(childName + "Parser")); } // enforce the inline form for collapsed elements @@ -148,7 +150,7 @@ private AbstractSchema createParser(Model m, MainInfo main, ElementInfo eleme // Allow object-level component reference if any setter expects a component. if (hasComponentChild(elementInfo, main) && !parser.hasProperty("$ref")) { - parser.property(string("$ref") + parser.property(string($_REF) .description("JSON Pointer to a component.") .required(false)); } @@ -315,7 +317,7 @@ private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSc .findFirst() .orElseThrow(); // should never happen due to shouldInlineListItems - AbstractSchema itemsSchema = ref(itemEi.getAnnotation().name()).ref("#/$defs/" + itemEi.getXSDTypeName(m)); + AbstractSchema itemsSchema = defsSchemaRef(itemEi.getAnnotation().name(), itemEi.getXSDTypeName(m)); // keep "- $ref: ..." alternative for component items if (!isComponentsList(i, cei) && itemEi.getAnnotation().component()) { @@ -324,7 +326,7 @@ private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSc variants.add(object() .title("componentRef") .additionalProperties(false) - .property(string("$ref").required(false))); + .property(string($_REF).required(false))); itemsSchema = anyOf(variants); } @@ -341,8 +343,7 @@ private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSc sos.add(object() .title(ei.getAnnotation().name()) .additionalProperties(false) - .property(ref(ei.getAnnotation().name()) - .ref("#/$defs/" + ei.getXSDTypeName(m)))); + .property(defsSchemaRef(ei.getAnnotation().name(), ei.getXSDTypeName(m)))); } // Allow referencing a component instance directly on list-item level: // flow: @@ -350,7 +351,7 @@ private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSc sos.add(object() .title("componentRef") .additionalProperties(false) - .property(string("$ref"))); + .property(string($_REF))); return sos; } @@ -437,10 +438,10 @@ private AbstractSchema processList(ElementInfo i, AbstractSchema so, Child private void addFlowParserRef(AbstractSchema parentSchema, String propertyName, List> sos) { if (!flowDefCreated) { - schema.definition(array("flowParser").items(anyOf(sos))); + schema.definition(array(FLOW_PARSER_DEF_NAME).items(anyOf(sos))); flowDefCreated = true; } - SchemaRef ref = ref(propertyName).ref("#/$defs/flowParser"); + SchemaRef ref = ref(propertyName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); if (parentSchema instanceof SchemaArray sa) { sa.items(ref); @@ -459,7 +460,7 @@ private void addChildsAsProperties(Model m, MainInfo main, ChildElementInfo cei, // If this list can contain at least one @MCElement(component=true) type, // allow "- $ref: ..." as an alternative list item shape. if (listItemContext && !componentsContext && eis.stream().anyMatch(ei -> ei.getAnnotation().component())) { - parent2.property(string("$ref").required(false)); + parent2.property(string($_REF).required(false)); } for (ElementInfo ei : eis) { @@ -471,7 +472,7 @@ private void addChildsAsProperties(Model m, MainInfo main, ChildElementInfo cei, } private static SchemaRef getRef(Model m, ElementInfo ei) { - return ref(ei.getAnnotation().name()).ref("#/$defs/" + ei.getXSDTypeName(m)); + return defsSchemaRef(ei.getAnnotation().name(), ei.getXSDTypeName(m)); } private static ChildElementDeclarationInfo getChildElementDeclarationInfo(MainInfo main, ChildElementInfo cei) { @@ -521,8 +522,7 @@ private SchemaObject createComponentsMapParser(Model m, MainInfo main, ElementIn .title(n) .additionalProperties(false) .minProperties(1) - .property(ref(n) - .ref("#/$defs/" + comp.getXSDTypeName(m)))); + .property(defsSchemaRef(n, comp.getXSDTypeName(m)))); } return variants; } @@ -583,6 +583,14 @@ private boolean hasAnyConfigurableProperty(ElementInfo ei, MainInfo main) { || hasComponentChild(ei, main); } + private static String defsRefPath(String defName) { + return JSON_SCHEMA_DEFS_PREFIX + defName; + } + + private static SchemaRef defsSchemaRef(String propertyName, String defName) { + return ref(propertyName).ref(defsRefPath(defName)); + } + // For description. Probably we'll include that later. (Temporarily deactivated!) private String getDescriptionAsText(AbstractJavadocedInfo elementInfo) { return escapeJsonContent(getDescriptionContent(elementInfo).replaceAll("<[^>]+>", "").replaceAll("\\s+", " ").trim()); From 145a90c89625d9a601f5c001ae5192e16cb96ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:28:56 +0100 Subject: [PATCH 03/10] Refactor component tracking in JSON schema generation to use Set instead of Map --- .../membrane/annot/generator/JsonSchemaGenerator.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 3797a97a3a..f68b924792 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -52,7 +52,7 @@ public class JsonSchemaGenerator extends AbstractGrammar { // TODO keep this pattern or allow *? public static final String COMPONENT_ID_PATTERN = "^[A-Za-z_][A-Za-z0-9_-]*$"; - private final Map componentAdded = new HashMap<>(); + private final Set componentParsersAdded = new HashSet<>(); private boolean flowDefCreated = false; private Schema schema; @@ -71,7 +71,7 @@ private void assemble(Model m, MainInfo main) throws IOException { // Reset so multiple calls would be possible flowDefCreated = false; schema = schema("membrane"); - componentAdded.clear(); + componentParsersAdded.clear(); addParserDefinitions(m, main); addTopLevelProperties(m, main); @@ -123,11 +123,11 @@ private AbstractSchema createParser(Model m, MainInfo main, ElementInfo eleme ChildElementInfo child = elementInfo.getChildElementSpecs().getFirst(); var childName = child.getPropertyName(); - if (!componentAdded.containsKey(childName) && !shouldGenerateFlowParserType(child)) { + if (!componentParsersAdded.contains(childName) && !shouldGenerateFlowParserType(child)) { SchemaArray array = array(childName + "Parser"); processMCChilds(m, main, child.getEi(), array); schema.definition(array); - componentAdded.put(childName, true); + componentParsersAdded.add(childName); } if (shouldGenerateFlowParserType(child)) { From fc92316f2b9ad91f494f52ceebc1a7ed6962d898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:34:42 +0100 Subject: [PATCH 04/10] Refactor method parameters in `JsonSchemaGenerator` for improved readability and clarity --- .../annot/generator/JsonSchemaGenerator.java | 224 +++++++++--------- 1 file changed, 113 insertions(+), 111 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index f68b924792..98b2e82efb 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -265,62 +265,62 @@ private AbstractSchema createProperty(AttributeInfo ai) { } - private void collectProperties(Model m, MainInfo main, ElementInfo i, SchemaObject schema) { - processMCAttributes(i, schema); - collectTextContent(i, schema); - processMCChilds(m, main, i, schema); + private void collectProperties(Model model, MainInfo main, ElementInfo elementInfo, SchemaObject parserSchema) { + processMCAttributes(elementInfo, parserSchema); + collectTextContent(elementInfo, parserSchema); + processMCChilds(model, main, elementInfo, parserSchema); } - private void collectTextContent(ElementInfo i, SchemaObject so) { - if (i.getTci() == null) + private void collectTextContent(ElementInfo elementInfo, SchemaObject parserSchema) { + if (elementInfo.getTci() == null) return; - var sop = string(i.getTci().getPropertyName()); - // sop.addAttribute("description", getDescriptionAsText(i)); - // sop.addAttribute("x-intellij-html-description", getDescriptionAsHtml(i)); - so.property(sop); + var textProperty = string(elementInfo.getTci().getPropertyName()); + // textProperty.addAttribute("description", getDescriptionAsText(elementInfo)); + // textProperty.addAttribute("x-intellij-html-description", getDescriptionAsHtml(elementInfo)); + parserSchema.property(textProperty); } - private void processMCChilds(Model m, MainInfo main, ElementInfo i, AbstractSchema so) { - for (ChildElementInfo cei : i.getChildElementSpecs()) { + private void processMCChilds(Model model, MainInfo main, ElementInfo parentElementInfo, AbstractSchema parentSchema) { + for (ChildElementInfo childSpec : parentElementInfo.getChildElementSpecs()) { - if (!cei.isList()) { - addChildsAsProperties(m, main, cei, (SchemaObject) so, isComponentsList(i, cei), false); + if (!childSpec.isList()) { + addChildsAsProperties(model, main, childSpec, (SchemaObject) parentSchema, isComponentsList(parentElementInfo, childSpec), false); continue; } - if (isScalarChildList(cei)) { - attachArrayItems(i, so, cei, scalarItemsSchema(cei)); + if (isScalarChildList(childSpec)) { + attachArrayItems(parentElementInfo, parentSchema, childSpec, scalarItemsSchema(childSpec)); continue; } - if (shouldGenerateFlowParserType(cei)) { - processList(i, so, cei, getSchemaObjects(m, main, cei)); + if (shouldGenerateFlowParserType(childSpec)) { + processList(parentElementInfo, parentSchema, childSpec, getSchemaObjects(model, main, childSpec)); continue; } - if (shouldInlineListItems(main, cei)) { - processInlineList(m, main, i, so, cei); + if (shouldInlineListItems(main, childSpec)) { + processInlineList(model, main, parentElementInfo, parentSchema, childSpec); continue; } - AbstractSchema parent2 = processList(i, so, cei, null); - addChildsAsProperties(m, main, cei, (SchemaObject) parent2, isComponentsList(i, cei), true); + AbstractSchema listItemObjectSchema = processList(parentElementInfo, parentSchema, childSpec, null); + addChildsAsProperties(model, main, childSpec, (SchemaObject) listItemObjectSchema, isComponentsList(parentElementInfo, childSpec), true); } } - private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSchema so, ChildElementInfo cei) { - var decl = getChildElementDeclarationInfo(main, cei); + private void processInlineList(Model model, MainInfo main, ElementInfo parentElementInfo, AbstractSchema parentSchema, ChildElementInfo childSpec) { + var childDeclaration = getChildElementDeclarationInfo(main, childSpec); - ElementInfo itemEi = decl.getElementInfo().stream() - .filter(ei -> !ei.getAnnotation().topLevel()) + ElementInfo itemElementInfo = childDeclaration.getElementInfo().stream() + .filter(candidateElementInfo -> !candidateElementInfo.getAnnotation().topLevel()) .findFirst() .orElseThrow(); // should never happen due to shouldInlineListItems - AbstractSchema itemsSchema = defsSchemaRef(itemEi.getAnnotation().name(), itemEi.getXSDTypeName(m)); + AbstractSchema itemsSchema = defsSchemaRef(itemElementInfo.getAnnotation().name(), itemElementInfo.getXSDTypeName(model)); // keep "- $ref: ..." alternative for component items - if (!isComponentsList(i, cei) && itemEi.getAnnotation().component()) { + if (!isComponentsList(parentElementInfo, childSpec) && itemElementInfo.getAnnotation().component()) { var variants = new ArrayList>(); variants.add(itemsSchema); variants.add(object() @@ -330,29 +330,30 @@ private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSc itemsSchema = anyOf(variants); } - attachArrayItems(i, so, cei, itemsSchema); + attachArrayItems(parentElementInfo, parentSchema, childSpec, itemsSchema); } - private static @NotNull ArrayList> getSchemaObjects(Model m, MainInfo main, ChildElementInfo cei) { - var sos = new ArrayList>(); + private static @NotNull ArrayList> getSchemaObjects(Model model, MainInfo main, ChildElementInfo childSpec) { + var variants = new ArrayList>(); - for (ElementInfo ei : main.getChildElementDeclarations().get(cei.getTypeDeclaration()).getElementInfo()) { - if (ei.getAnnotation().excludeFromFlow()) + for (ElementInfo candidateElementInfo : main.getChildElementDeclarations().get(childSpec.getTypeDeclaration()).getElementInfo()) { + if (candidateElementInfo.getAnnotation().excludeFromFlow()) continue; - sos.add(object() - .title(ei.getAnnotation().name()) + variants.add(object() + .title(candidateElementInfo.getAnnotation().name()) .additionalProperties(false) - .property(defsSchemaRef(ei.getAnnotation().name(), ei.getXSDTypeName(m)))); + .property(defsSchemaRef(candidateElementInfo.getAnnotation().name(), candidateElementInfo.getXSDTypeName(model)))); } + // Allow referencing a component instance directly on list-item level: // flow: // - $ref: ... - sos.add(object() + variants.add(object() .title("componentRef") .additionalProperties(false) .property(string($_REF))); - return sos; + return variants; } private boolean isScalarChildList(ChildElementInfo cei) { @@ -393,10 +394,10 @@ private AbstractSchema scalarItemsSchema(ChildElementInfo cei) { return SchemaFactory.from("string").type("string"); } - private boolean isComponentsList(ElementInfo parent, ChildElementInfo cei) { - return COMPONENTS.equals(parent.getAnnotation().name()) - && parent.getAnnotation().noEnvelope() - && COMPONENTS.equals(cei.getPropertyName()); + private boolean isComponentsList(ElementInfo parentElementInfo, ChildElementInfo childSpec) { + return COMPONENTS.equals(parentElementInfo.getAnnotation().name()) + && parentElementInfo.getAnnotation().noEnvelope() + && COMPONENTS.equals(childSpec.getPropertyName()); } private boolean shouldGenerateFlowParserType(ChildElementInfo cei) { @@ -415,79 +416,78 @@ boolean isFlowFromWebSocket(ChildElementInfo cei) { return "com.predic8.membrane.core.transport.ws.WebSocketInterceptorInterface".equals(cei.getTypeDeclaration().getQualifiedName().toString()); } - private AbstractSchema processList(ElementInfo i, AbstractSchema so, ChildElementInfo cei, ArrayList> sos) { - SchemaObject items = object("items"); + private AbstractSchema processList(ElementInfo parentElementInfo, AbstractSchema parentSchema, ChildElementInfo childSpec, ArrayList> itemVariants) { + SchemaObject itemsObjectSchema = object("items"); - if (shouldGenerateFlowParserType(cei)) { - addFlowParserRef(so, cei.getPropertyName(), sos); - return items; + if (shouldGenerateFlowParserType(childSpec)) { + addFlowParserRef(parentSchema, childSpec.getPropertyName(), itemVariants); + return itemsObjectSchema; } - items.type("object").additionalProperties(cei.getAnnotation().allowForeign()); + itemsObjectSchema.type("object").additionalProperties(childSpec.getAnnotation().allowForeign()); - if (i.getAnnotation().noEnvelope() && so instanceof SchemaArray sa) { - sa.items(items); + if (parentElementInfo.getAnnotation().noEnvelope() && parentSchema instanceof SchemaArray schemaArray) { + schemaArray.items(itemsObjectSchema); } else { - if (so instanceof SchemaObject sObj) { - sObj.property(createFromChild(cei, items)); + if (parentSchema instanceof SchemaObject schemaObject) { + schemaObject.property(createFromChild(childSpec, itemsObjectSchema)); } } - return items; + return itemsObjectSchema; } - private void addFlowParserRef(AbstractSchema parentSchema, String propertyName, List> sos) { + private void addFlowParserRef(AbstractSchema parentSchema, String propertyName, List> itemVariants) { if (!flowDefCreated) { - schema.definition(array(FLOW_PARSER_DEF_NAME).items(anyOf(sos))); + schema.definition(array(FLOW_PARSER_DEF_NAME).items(anyOf(itemVariants))); flowDefCreated = true; } - SchemaRef ref = ref(propertyName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); + SchemaRef flowParserRef = ref(propertyName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); - if (parentSchema instanceof SchemaArray sa) { - sa.items(ref); - } else if (parentSchema instanceof SchemaObject sObj) { - sObj.property(ref); + if (parentSchema instanceof SchemaArray schemaArray) { + schemaArray.items(flowParserRef); + } else if (parentSchema instanceof SchemaObject schemaObject) { + schemaObject.property(flowParserRef); } } - private void addChildsAsProperties(Model m, MainInfo main, ChildElementInfo cei, SchemaObject parent2, boolean componentsContext, boolean listItemContext) { - var eis = getChildElementDeclarationInfo(main, cei).getElementInfo().stream() + private void addChildsAsProperties(Model model, MainInfo main, ChildElementInfo childSpec, SchemaObject parentObjectSchema, boolean componentsContext, boolean listItemContext) { + var childElementInfos = getChildElementDeclarationInfo(main, childSpec).getElementInfo().stream() // Top-level elements cannot be configurable as nested children - .filter(ei -> !ei.getAnnotation().topLevel()) + .filter(candidateElementInfo -> !candidateElementInfo.getAnnotation().topLevel()) .toList(); // Generic list-item reference support: // If this list can contain at least one @MCElement(component=true) type, // allow "- $ref: ..." as an alternative list item shape. - if (listItemContext && !componentsContext && eis.stream().anyMatch(ei -> ei.getAnnotation().component())) { - parent2.property(string($_REF).required(false)); + if (listItemContext && !componentsContext && childElementInfos.stream().anyMatch(candidateElementInfo -> candidateElementInfo.getAnnotation().component())) { + parentObjectSchema.property(string($_REF).required(false)); } - for (ElementInfo ei : eis) { - - parent2.property(getRef(m, ei)) - .description(getDescriptionContent(ei)) - .required(cei.isRequired()); + for (ElementInfo childElementInfo : childElementInfos) { + parentObjectSchema.property(getRef(model, childElementInfo)) + .description(getDescriptionContent(childElementInfo)) + .required(childSpec.isRequired()); } } - private static SchemaRef getRef(Model m, ElementInfo ei) { - return defsSchemaRef(ei.getAnnotation().name(), ei.getXSDTypeName(m)); + private static SchemaRef getRef(Model model, ElementInfo elementInfo) { + return defsSchemaRef(elementInfo.getAnnotation().name(), elementInfo.getXSDTypeName(model)); } - private static ChildElementDeclarationInfo getChildElementDeclarationInfo(MainInfo main, ChildElementInfo cei) { - return getChildElementDeclarations(main).get(cei.getTypeDeclaration()); + private static ChildElementDeclarationInfo getChildElementDeclarationInfo(MainInfo main, ChildElementInfo childSpec) { + return getChildElementDeclarations(main).get(childSpec.getTypeDeclaration()); } private static Map getChildElementDeclarations(MainInfo main) { return main.getChildElementDeclarations(); } - private SchemaArray createFromChild(ChildElementInfo cei, SchemaObject items) { - return array(cei.getPropertyName()) - .items(items) - .required(cei.isRequired()) - .description(getDescriptionContent(cei)); + private SchemaArray createFromChild(ChildElementInfo childSpec, SchemaObject itemsObjectSchema) { + return array(childSpec.getPropertyName()) + .items(itemsObjectSchema) + .required(childSpec.isRequired()) + .description(getDescriptionContent(childSpec)); } @Override @@ -527,60 +527,62 @@ private SchemaObject createComponentsMapParser(Model m, MainInfo main, ElementIn return variants; } - private boolean hasComponentChild(ElementInfo parent, MainInfo main) { - for (ChildElementInfo cei : parent.getChildElementSpecs()) { - var decl = getChildElementDeclarationInfo(main, cei); - if (decl == null) continue; + private boolean hasComponentChild(ElementInfo parentElementInfo, MainInfo main) { + for (ChildElementInfo childSpec : parentElementInfo.getChildElementSpecs()) { + var childDeclaration = getChildElementDeclarationInfo(main, childSpec); + if (childDeclaration == null) continue; - if (decl.getElementInfo().stream().anyMatch(ei -> ei.getAnnotation().component())) + if (childDeclaration.getElementInfo().stream().anyMatch(candidateElementInfo -> candidateElementInfo.getAnnotation().component())) return true; } return false; } - private boolean shouldInlineListItems(MainInfo main, ChildElementInfo cei) { - if (!cei.isList()) return false; - if (cei.getAnnotation().allowForeign()) return false; + private boolean shouldInlineListItems(MainInfo main, ChildElementInfo childSpec) { + if (!childSpec.isList()) return false; + if (childSpec.getAnnotation().allowForeign()) return false; - var decl = getChildElementDeclarationInfo(main, cei); - if (decl == null) return false; + var childDeclaration = getChildElementDeclarationInfo(main, childSpec); + if (childDeclaration == null) return false; - var eis = decl.getElementInfo().stream().filter(ei -> !ei.getAnnotation().topLevel()).toList(); + var candidateElementInfos = childDeclaration.getElementInfo().stream() + .filter(candidateElementInfo -> !candidateElementInfo.getAnnotation().topLevel()) + .toList(); // Only inline if there is exactly ONE possible list-item element type (no inheritance etc.) - if (eis.size() != 1) return false; + if (candidateElementInfos.size() != 1) return false; - var ei = eis.getFirst(); + var itemElementInfo = candidateElementInfos.getFirst(); - if (ei.getAnnotation().collapsed()) return false; - if (ei.getAnnotation().noEnvelope()) return false; + if (itemElementInfo.getAnnotation().collapsed()) return false; + if (itemElementInfo.getAnnotation().noEnvelope()) return false; - return hasAnyConfigurableProperty(ei, main); + return hasAnyConfigurableProperty(itemElementInfo, main); } - private void attachArrayItems(ElementInfo parentEi, AbstractSchema parentSchema, ChildElementInfo cei, AbstractSchema itemsSchema) { + private void attachArrayItems(ElementInfo parentElementInfo, AbstractSchema parentSchema, ChildElementInfo childSpec, AbstractSchema arrayItemsSchema) { // noEnvelope list: parent is an array already - if (parentEi.getAnnotation().noEnvelope() && parentSchema instanceof SchemaArray sa) { - sa.items(itemsSchema); + if (parentElementInfo.getAnnotation().noEnvelope() && parentSchema instanceof SchemaArray schemaArray) { + schemaArray.items(arrayItemsSchema); return; } - if (parentSchema instanceof SchemaObject so) { - so.property(array(cei.getPropertyName()) - .items(itemsSchema) - .required(cei.isRequired()) - .description(getDescriptionContent(cei))); + if (parentSchema instanceof SchemaObject schemaObject) { + schemaObject.property(array(childSpec.getPropertyName()) + .items(arrayItemsSchema) + .required(childSpec.isRequired()) + .description(getDescriptionContent(childSpec))); } } - private boolean hasAnyConfigurableProperty(ElementInfo ei, MainInfo main) { - return ei.getAis().stream() - .filter(ai -> !ai.excludedFromJsonSchema()) - .anyMatch(ai -> !"id".equals(ai.getXMLName())) - || ei.getTci() != null - || !ei.getChildElementSpecs().isEmpty() - || ei.getOai() != null - || hasComponentChild(ei, main); + private boolean hasAnyConfigurableProperty(ElementInfo elementInfo, MainInfo main) { + return elementInfo.getAis().stream() + .filter(attributeInfo -> !attributeInfo.excludedFromJsonSchema()) + .anyMatch(attributeInfo -> !"id".equals(attributeInfo.getXMLName())) + || elementInfo.getTci() != null + || !elementInfo.getChildElementSpecs().isEmpty() + || elementInfo.getOai() != null + || hasComponentChild(elementInfo, main); } private static String defsRefPath(String defName) { From 4f1af535578e6be7185a28f7c112f7e871ff896f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:38:09 +0100 Subject: [PATCH 05/10] Refactor `JsonSchemaGenerator` to extract specialized parsers for improved readability and maintainability --- .../annot/generator/JsonSchemaGenerator.java | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 98b2e82efb..4a74e9b1be 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -110,52 +110,67 @@ private void addParserDefinitions(Model m, MainInfo main) { } } - private AbstractSchema createParser(Model m, MainInfo main, ElementInfo elementInfo) { - String parserName = elementInfo.getXSDTypeName(m); + private AbstractSchema createParser(Model model, MainInfo main, ElementInfo elementInfo) { + String parserName = elementInfo.getXSDTypeName(model); if (isComponentsMap(elementInfo)) { - return createComponentsMapParser(m, main, elementInfo, parserName); + return createComponentsMapParser(model, main, elementInfo, parserName); } - // e.g. to prevent a request from needing a flow child noEnvelope=true is used if (elementInfo.getAnnotation().noEnvelope()) { - // With noEnvelope=true, there should be exactly one child element - ChildElementInfo child = elementInfo.getChildElementSpecs().getFirst(); - var childName = child.getPropertyName(); - - if (!componentParsersAdded.contains(childName) && !shouldGenerateFlowParserType(child)) { - SchemaArray array = array(childName + "Parser"); - processMCChilds(m, main, child.getEi(), array); - schema.definition(array); - componentParsersAdded.add(childName); - } - - if (shouldGenerateFlowParserType(child)) { - return ref(parserName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); - } - return ref(parserName).ref(defsRefPath(childName + "Parser")); + return createNoEnvelopeParser(model, main, elementInfo, parserName); } // enforce the inline form for collapsed elements if (elementInfo.getAnnotation().collapsed()) { - if (elementInfo.getAnnotation().noEnvelope()) { - throw new ProcessingException("@MCElement(collapsed=true) is not compatible with noEnvelope=true.", elementInfo.getElement()); - } - return createCollapsedInlineParser(elementInfo, parserName); + return createCollapsedParser(elementInfo, parserName); } - SchemaObject parser = getParserSchemaObject(elementInfo, parserName); + return createRegularParser(model, main, elementInfo, parserName); + } + + private AbstractSchema createNoEnvelopeParser(Model model, MainInfo main, ElementInfo elementInfo, String parserName) { + // With noEnvelope=true, there should be exactly one child element + ChildElementInfo childSpec = elementInfo.getChildElementSpecs().getFirst(); + String childName = childSpec.getPropertyName(); + + if (!componentParsersAdded.contains(childName) && !shouldGenerateFlowParserType(childSpec)) { + SchemaArray childParserArray = array(childName + "Parser"); + processMCChilds(model, main, childSpec.getEi(), childParserArray); + schema.definition(childParserArray); + componentParsersAdded.add(childName); + } - collectProperties(m, main, elementInfo, parser); + if (shouldGenerateFlowParserType(childSpec)) { + return ref(parserName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); + } + + return ref(parserName).ref(defsRefPath(childName + "Parser")); + } + + private AbstractSchema createCollapsedParser(ElementInfo elementInfo, String parserName) { + if (elementInfo.getAnnotation().noEnvelope()) { + throw new ProcessingException( + "@MCElement(collapsed=true) is not compatible with noEnvelope=true.", + elementInfo.getElement() + ); + } + return createCollapsedInlineParser(elementInfo, parserName); + } + + private AbstractSchema createRegularParser(Model model, MainInfo main, ElementInfo elementInfo, String parserName) { + SchemaObject parserSchema = getParserSchemaObject(elementInfo, parserName); + + collectProperties(model, main, elementInfo, parserSchema); // Allow object-level component reference if any setter expects a component. - if (hasComponentChild(elementInfo, main) && !parser.hasProperty("$ref")) { - parser.property(string($_REF) + if (hasComponentChild(elementInfo, main) && !parserSchema.hasProperty($_REF)) { + parserSchema.property(string($_REF) .description("JSON Pointer to a component.") .required(false)); } - return parser; + return parserSchema; } private AbstractSchema createCollapsedInlineParser(ElementInfo ei, String parserName) { From 99aeb4adf8ed7b8d89e7ce45fd9b863c8bfd3016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:41:39 +0100 Subject: [PATCH 06/10] Refactor `JsonSchemaGenerator` to introduce `ScalarSchemaKind` for improved type handling and code clarity --- .../annot/generator/JsonSchemaGenerator.java | 67 +++++++++---------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 4a74e9b1be..3660434923 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -371,42 +371,17 @@ private void processInlineList(Model model, MainInfo main, ElementInfo parentEle return variants; } - private boolean isScalarChildList(ChildElementInfo cei) { - if (!cei.isList()) return false; - if (cei.getTypeDeclaration() == null) return false; - - String qn = cei.getTypeDeclaration().getQualifiedName().toString(); - - return "java.lang.String".equals(qn) - || "java.lang.Boolean".equals(qn) - || "java.lang.Integer".equals(qn) - || "java.lang.Long".equals(qn) - || "java.lang.Double".equals(qn) - || "java.lang.Float".equals(qn) - || "java.lang.Short".equals(qn) - || "java.lang.Byte".equals(qn); + private boolean isScalarChildList(ChildElementInfo childSpec) { + return childSpec.isList() && getScalarSchemaKind(childSpec) != ScalarSchemaKind.NONE; } - private AbstractSchema scalarItemsSchema(ChildElementInfo cei) { - String qn = cei.getTypeDeclaration().getQualifiedName().toString(); - - if ("java.lang.String".equals(qn)) { - return SchemaFactory.from("string").type("string"); - } - if ("java.lang.Boolean".equals(qn)) { - return SchemaFactory.from("boolean").type("boolean"); - } - if ("java.lang.Integer".equals(qn) - || "java.lang.Long".equals(qn) - || "java.lang.Short".equals(qn) - || "java.lang.Byte".equals(qn)) { - return SchemaFactory.from("integer").type("integer"); - } - if ("java.lang.Double".equals(qn) || "java.lang.Float".equals(qn)) { - return SchemaFactory.from("number").type("number"); - } - - return SchemaFactory.from("string").type("string"); + private AbstractSchema scalarItemsSchema(ChildElementInfo childSpec) { + return switch (getScalarSchemaKind(childSpec)) { + case STRING, NONE -> SchemaFactory.from("string").type("string"); + case BOOLEAN -> SchemaFactory.from("boolean").type("boolean"); + case INTEGER -> SchemaFactory.from("integer").type("integer"); + case NUMBER -> SchemaFactory.from("number").type("number"); + }; } private boolean isComponentsList(ElementInfo parentElementInfo, ChildElementInfo childSpec) { @@ -608,6 +583,30 @@ private static SchemaRef defsSchemaRef(String propertyName, String defName) { return ref(propertyName).ref(defsRefPath(defName)); } + private enum ScalarSchemaKind { + NONE, + STRING, + BOOLEAN, + INTEGER, + NUMBER + } + + private ScalarSchemaKind getScalarSchemaKind(ChildElementInfo childSpec) { + if (childSpec.getTypeDeclaration() == null) { + return ScalarSchemaKind.NONE; + } + + String qualifiedName = childSpec.getTypeDeclaration().getQualifiedName().toString(); + + return switch (qualifiedName) { + case "java.lang.String" -> ScalarSchemaKind.STRING; + case "java.lang.Boolean" -> ScalarSchemaKind.BOOLEAN; + case "java.lang.Integer", "java.lang.Long", "java.lang.Short", "java.lang.Byte" -> ScalarSchemaKind.INTEGER; + case "java.lang.Double", "java.lang.Float" -> ScalarSchemaKind.NUMBER; + default -> ScalarSchemaKind.NONE; + }; + } + // For description. Probably we'll include that later. (Temporarily deactivated!) private String getDescriptionAsText(AbstractJavadocedInfo elementInfo) { return escapeJsonContent(getDescriptionContent(elementInfo).replaceAll("<[^>]+>", "").replaceAll("\\s+", " ").trim()); From 26342d550bc5567371be660bae28e1ffe5a21f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:46:06 +0100 Subject: [PATCH 07/10] Refactor `JsonSchemaGenerator` to extract common logic for handling array items and object properties into helper methods for improved readability and maintainability --- .../annot/generator/JsonSchemaGenerator.java | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 3660434923..d05582b772 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -416,13 +416,10 @@ private AbstractSchema processList(ElementInfo parentElementInfo, AbstractSch itemsObjectSchema.type("object").additionalProperties(childSpec.getAnnotation().allowForeign()); - if (parentElementInfo.getAnnotation().noEnvelope() && parentSchema instanceof SchemaArray schemaArray) { - schemaArray.items(itemsObjectSchema); - } else { - if (parentSchema instanceof SchemaObject schemaObject) { - schemaObject.property(createFromChild(childSpec, itemsObjectSchema)); - } + if (parentElementInfo.getAnnotation().noEnvelope()) { + setItemsIfArray(parentSchema, itemsObjectSchema); } + addPropertyIfObject(parentSchema, createFromChild(childSpec, itemsObjectSchema)); return itemsObjectSchema; } @@ -434,11 +431,8 @@ private void addFlowParserRef(AbstractSchema parentSchema, String propertyNam } SchemaRef flowParserRef = ref(propertyName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); - if (parentSchema instanceof SchemaArray schemaArray) { - schemaArray.items(flowParserRef); - } else if (parentSchema instanceof SchemaObject schemaObject) { - schemaObject.property(flowParserRef); - } + setItemsIfArray(parentSchema, flowParserRef); + addPropertyIfObject(parentSchema, flowParserRef); } private void addChildsAsProperties(Model model, MainInfo main, ChildElementInfo childSpec, SchemaObject parentObjectSchema, boolean componentsContext, boolean listItemContext) { @@ -552,17 +546,14 @@ private boolean shouldInlineListItems(MainInfo main, ChildElementInfo childSpec) private void attachArrayItems(ElementInfo parentElementInfo, AbstractSchema parentSchema, ChildElementInfo childSpec, AbstractSchema arrayItemsSchema) { // noEnvelope list: parent is an array already - if (parentElementInfo.getAnnotation().noEnvelope() && parentSchema instanceof SchemaArray schemaArray) { - schemaArray.items(arrayItemsSchema); + if (parentElementInfo.getAnnotation().noEnvelope()) { + setItemsIfArray(parentSchema, arrayItemsSchema); return; } - - if (parentSchema instanceof SchemaObject schemaObject) { - schemaObject.property(array(childSpec.getPropertyName()) - .items(arrayItemsSchema) - .required(childSpec.isRequired()) - .description(getDescriptionContent(childSpec))); - } + addPropertyIfObject(parentSchema, array(childSpec.getPropertyName()) + .items(arrayItemsSchema) + .required(childSpec.isRequired()) + .description(getDescriptionContent(childSpec))); } private boolean hasAnyConfigurableProperty(ElementInfo elementInfo, MainInfo main) { @@ -575,6 +566,18 @@ private boolean hasAnyConfigurableProperty(ElementInfo elementInfo, MainInfo mai || hasComponentChild(elementInfo, main); } + private void setItemsIfArray(AbstractSchema parentSchema, AbstractSchema itemsSchema) { + if (parentSchema instanceof SchemaArray schemaArray) { + schemaArray.items(itemsSchema); + } + } + + private void addPropertyIfObject(AbstractSchema parentSchema, AbstractSchema propertySchema) { + if (parentSchema instanceof SchemaObject schemaObject) { + schemaObject.property(propertySchema); + } + } + private static String defsRefPath(String defName) { return JSON_SCHEMA_DEFS_PREFIX + defName; } From 1527cd5c37098f656ce7b4dcfebabafebe1147d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 10:50:22 +0100 Subject: [PATCH 08/10] Refactor `JsonSchemaGenerator` to extract `componentRefVariantSchema` method for improved code reuse and readability --- .../annot/generator/JsonSchemaGenerator.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index d05582b772..0d19759c05 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -338,10 +338,7 @@ private void processInlineList(Model model, MainInfo main, ElementInfo parentEle if (!isComponentsList(parentElementInfo, childSpec) && itemElementInfo.getAnnotation().component()) { var variants = new ArrayList>(); variants.add(itemsSchema); - variants.add(object() - .title("componentRef") - .additionalProperties(false) - .property(string($_REF).required(false))); + variants.add(componentRefVariantSchema(false)); itemsSchema = anyOf(variants); } @@ -364,10 +361,7 @@ private void processInlineList(Model model, MainInfo main, ElementInfo parentEle // Allow referencing a component instance directly on list-item level: // flow: // - $ref: ... - variants.add(object() - .title("componentRef") - .additionalProperties(false) - .property(string($_REF))); + variants.add(componentRefVariantSchema()); return variants; } @@ -586,6 +580,22 @@ private static SchemaRef defsSchemaRef(String propertyName, String defName) { return ref(propertyName).ref(defsRefPath(defName)); } + private static SchemaObject componentRefVariantSchema() { + return componentRefVariantSchema(null); + } + + private static SchemaObject componentRefVariantSchema(Boolean required) { + var refProperty = string($_REF); + if (required != null) { + refProperty.required(required); + } + + return object() + .title("componentRef") + .additionalProperties(false) + .property(refProperty); + } + private enum ScalarSchemaKind { NONE, STRING, From c6426601b91995e2a16d14df47c46360323382c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 11:07:50 +0100 Subject: [PATCH 09/10] Refactor `JsonSchemaGenerator` to replace `Set` with `Map` for tracking parser sources and improve declaration handling logic --- .../annot/generator/JsonSchemaGenerator.java | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 0d19759c05..733987ea65 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -52,7 +52,7 @@ public class JsonSchemaGenerator extends AbstractGrammar { // TODO keep this pattern or allow *? public static final String COMPONENT_ID_PATTERN = "^[A-Za-z_][A-Za-z0-9_-]*$"; - private final Set componentParsersAdded = new HashSet<>(); + private final Map noEnvelopeParserSourceByDefName = new HashMap<>(); private boolean flowDefCreated = false; private Schema schema; @@ -71,7 +71,7 @@ private void assemble(Model m, MainInfo main) throws IOException { // Reset so multiple calls would be possible flowDefCreated = false; schema = schema("membrane"); - componentParsersAdded.clear(); + noEnvelopeParserSourceByDefName.clear(); addParserDefinitions(m, main); addTopLevelProperties(m, main); @@ -134,18 +134,29 @@ private AbstractSchema createNoEnvelopeParser(Model model, MainInfo main, Ele ChildElementInfo childSpec = elementInfo.getChildElementSpecs().getFirst(); String childName = childSpec.getPropertyName(); - if (!componentParsersAdded.contains(childName) && !shouldGenerateFlowParserType(childSpec)) { - SchemaArray childParserArray = array(childName + "Parser"); - processMCChilds(model, main, childSpec.getEi(), childParserArray); - schema.definition(childParserArray); - componentParsersAdded.add(childName); + boolean flowParserType = shouldGenerateFlowParserType(childSpec); + if (flowParserType) { + return ref(parserName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); } - if (shouldGenerateFlowParserType(childSpec)) { - return ref(parserName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); + String defName = childName + "Parser"; + String sourceType = childSpec.getTypeDeclaration() == null ? "" : childSpec.getTypeDeclaration().getQualifiedName().toString(); + + String previousSourceType = noEnvelopeParserSourceByDefName.putIfAbsent(defName, sourceType); + if (previousSourceType != null && !previousSourceType.equals(sourceType)) { + throw new IllegalStateException( + "Conflicting noEnvelope parser definition '%s': %s vs %s" + .formatted(defName, previousSourceType, sourceType) + ); + } + + if (previousSourceType == null) { + SchemaArray childParserArray = array(defName); + processMCChilds(model, main, childSpec.getEi(), childParserArray); + schema.definition(childParserArray); } - return ref(parserName).ref(defsRefPath(childName + "Parser")); + return ref(parserName).ref(defsRefPath(defName)); } private AbstractSchema createCollapsedParser(ElementInfo elementInfo, String parserName) { @@ -325,7 +336,7 @@ private void processMCChilds(Model model, MainInfo main, ElementInfo parentEleme } private void processInlineList(Model model, MainInfo main, ElementInfo parentElementInfo, AbstractSchema parentSchema, ChildElementInfo childSpec) { - var childDeclaration = getChildElementDeclarationInfo(main, childSpec); + var childDeclaration = requireChildElementDeclarationInfo(main, childSpec); ElementInfo itemElementInfo = childDeclaration.getElementInfo().stream() .filter(candidateElementInfo -> !candidateElementInfo.getAnnotation().topLevel()) @@ -338,7 +349,7 @@ private void processInlineList(Model model, MainInfo main, ElementInfo parentEle if (!isComponentsList(parentElementInfo, childSpec) && itemElementInfo.getAnnotation().component()) { var variants = new ArrayList>(); variants.add(itemsSchema); - variants.add(componentRefVariantSchema(false)); + variants.add(componentRefVariantSchema(true)); itemsSchema = anyOf(variants); } @@ -348,7 +359,7 @@ private void processInlineList(Model model, MainInfo main, ElementInfo parentEle private static @NotNull ArrayList> getSchemaObjects(Model model, MainInfo main, ChildElementInfo childSpec) { var variants = new ArrayList>(); - for (ElementInfo candidateElementInfo : main.getChildElementDeclarations().get(childSpec.getTypeDeclaration()).getElementInfo()) { + for (ElementInfo candidateElementInfo : requireChildElementDeclarationInfo(main, childSpec).getElementInfo()) { if (candidateElementInfo.getAnnotation().excludeFromFlow()) continue; @@ -365,6 +376,19 @@ private void processInlineList(Model model, MainInfo main, ElementInfo parentEle return variants; } + private static ChildElementDeclarationInfo requireChildElementDeclarationInfo(MainInfo main, ChildElementInfo childSpec) { + ChildElementDeclarationInfo decl = getChildElementDeclarationInfo(main, childSpec); + if (decl != null) { + return decl; + } + throw new IllegalStateException( + "Missing child element declaration for child property '%s' (type: %s)." + .formatted(childSpec.getPropertyName(), childSpec.getTypeDeclaration() == null + ? "" + : childSpec.getTypeDeclaration().getQualifiedName().toString()) + ); + } + private boolean isScalarChildList(ChildElementInfo childSpec) { return childSpec.isList() && getScalarSchemaKind(childSpec) != ScalarSchemaKind.NONE; } @@ -412,9 +436,9 @@ private AbstractSchema processList(ElementInfo parentElementInfo, AbstractSch if (parentElementInfo.getAnnotation().noEnvelope()) { setItemsIfArray(parentSchema, itemsObjectSchema); + return itemsObjectSchema; } addPropertyIfObject(parentSchema, createFromChild(childSpec, itemsObjectSchema)); - return itemsObjectSchema; } @@ -430,8 +454,7 @@ private void addFlowParserRef(AbstractSchema parentSchema, String propertyNam } private void addChildsAsProperties(Model model, MainInfo main, ChildElementInfo childSpec, SchemaObject parentObjectSchema, boolean componentsContext, boolean listItemContext) { - var childElementInfos = getChildElementDeclarationInfo(main, childSpec).getElementInfo().stream() - // Top-level elements cannot be configurable as nested children + var childElementInfos = requireChildElementDeclarationInfo(main, childSpec).getElementInfo().stream() .filter(candidateElementInfo -> !candidateElementInfo.getAnnotation().topLevel()) .toList(); @@ -581,7 +604,7 @@ private static SchemaRef defsSchemaRef(String propertyName, String defName) { } private static SchemaObject componentRefVariantSchema() { - return componentRefVariantSchema(null); + return componentRefVariantSchema(true); } private static SchemaObject componentRefVariantSchema(Boolean required) { From f1386d898fcbedbcf489d4ef978d1eefdcf0fd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6rdes?= Date: Tue, 24 Feb 2026 11:18:22 +0100 Subject: [PATCH 10/10] Refactor `JsonSchemaGenerator` to extract `ensureFlowParserDefinition` and improve flow parser handling logic --- .../annot/generator/JsonSchemaGenerator.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 733987ea65..fbd4212ce6 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -136,6 +136,7 @@ private AbstractSchema createNoEnvelopeParser(Model model, MainInfo main, Ele boolean flowParserType = shouldGenerateFlowParserType(childSpec); if (flowParserType) { + ensureFlowParserDefinition(getSchemaObjects(model, main, childSpec)); return ref(parserName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); } @@ -156,7 +157,7 @@ private AbstractSchema createNoEnvelopeParser(Model model, MainInfo main, Ele schema.definition(childParserArray); } - return ref(parserName).ref(defsRefPath(defName)); + return ref(parserName).ref(defsRefPath(childName + "Parser")); } private AbstractSchema createCollapsedParser(ElementInfo elementInfo, String parserName) { @@ -311,7 +312,9 @@ private void processMCChilds(Model model, MainInfo main, ElementInfo parentEleme for (ChildElementInfo childSpec : parentElementInfo.getChildElementSpecs()) { if (!childSpec.isList()) { - addChildsAsProperties(model, main, childSpec, (SchemaObject) parentSchema, isComponentsList(parentElementInfo, childSpec), false); + if (parentSchema instanceof SchemaObject parentObjectSchema) { + addChildsAsProperties(model, main, childSpec, parentObjectSchema, isComponentsList(parentElementInfo, childSpec), false); + } continue; } @@ -443,12 +446,8 @@ private AbstractSchema processList(ElementInfo parentElementInfo, AbstractSch } private void addFlowParserRef(AbstractSchema parentSchema, String propertyName, List> itemVariants) { - if (!flowDefCreated) { - schema.definition(array(FLOW_PARSER_DEF_NAME).items(anyOf(itemVariants))); - flowDefCreated = true; - } + ensureFlowParserDefinition(itemVariants); SchemaRef flowParserRef = ref(propertyName).ref(defsRefPath(FLOW_PARSER_DEF_NAME)); - setItemsIfArray(parentSchema, flowParserRef); addPropertyIfObject(parentSchema, flowParserRef); } @@ -643,6 +642,15 @@ private ScalarSchemaKind getScalarSchemaKind(ChildElementInfo childSpec) { }; } + private void ensureFlowParserDefinition(List> itemVariants) { + if (flowDefCreated) { + return; + } + + schema.definition(array(FLOW_PARSER_DEF_NAME).items(anyOf(itemVariants))); + flowDefCreated = true; + } + // For description. Probably we'll include that later. (Temporarily deactivated!) private String getDescriptionAsText(AbstractJavadocedInfo elementInfo) { return escapeJsonContent(getDescriptionContent(elementInfo).replaceAll("<[^>]+>", "").replaceAll("\\s+", " ").trim());