private String generateObject(List<Map.Entry<String, JsonNode>> properties, Set<String> required, String name,
                                  JsonNode additionalProperties) {
        var propertyRules = new HashMap<String, String>();

        for (var property : properties) {
            var propertyKey = property.getKey();
            var propertySchema = property.getValue();
            var propertyValue = visit(propertySchema, (name == null ? "" : name + "-") + propertyKey);
            var propertyRuleName = (name == null ? "" : name + "-") + propertyKey + "-kv";
            var propertyRuleContent = formatLiteral("\\\"" + propertyKey + "\\\"") + " space \":\" space " + propertyValue;
            var propertyRule =  addRule(propertyRuleName, propertyRuleContent);
            propertyRules.put(propertyKey, propertyRule);
        }

        var requiredProperties = properties.stream()
                .map(Map.Entry::getKey)
                .filter(required::contains)
                .toList();

        var optionalProperties = properties.stream()
                .map(Map.Entry::getKey)
                .filter(k -> !required.contains(k))
                .collect(Collectors.toList());

        if (additionalProperties != null && (!additionalProperties.isBoolean() || additionalProperties.asBoolean())) {
            var subName = (name == null ? "" : name + "-") + "additional";

            var valueRule = additionalProperties.isObject()
                    ? visit(additionalProperties, subName + "-value")
                    : addPrimitive("value", PRIMITIVE_RULES.get("value"));

            var keyRule = properties.isEmpty()
                    ? addPrimitive("string", PRIMITIVE_RULES.get("string"))
                    : addRule(
                    subName + "-k",
                    generateNotStrings(properties.stream().map(Map.Entry::getKey).collect(Collectors.toSet()))
            );

            propertyRules.put("*",
                    addRule(subName + "-kv", keyRule + " \":\" space " + valueRule));
            optionalProperties.add("*");
        }

        var content = new StringBuilder("\"{\" space ");
        content.append(requiredProperties.stream().map(propertyRules::get)
                .collect(Collectors.joining(" \",\" space ")));

        if (!optionalProperties.isEmpty()) {
            content.append(" (");

            if (!requiredProperties.isEmpty()) {
                content.append(" \",\" space ( ");
            }

            content.append(optionalProperties.stream()
                    .map(k -> generateObjectRuleGetRecursiveRefs(
                            name, propertyRules,
                            optionalProperties.subList(optionalProperties.indexOf(k), optionalProperties.size()),
                            false
                    ))
                    .collect(Collectors.joining(" | ")));

            if (!requiredProperties.isEmpty()) {
                content.append(" )");
            }

            content.append(" )?");
        }

        content.append(" \"}\" space");
        return content.toString();
    }
Converts a JSON Schema to a GBNF grammar to be used for structured output with llama.cpp.
It is a port of Python implementation from llama.cpp repository.
Current implementation doesn't support regex patterns, resolving $ref or fetching schemas from URLs.
The plan is to replace this with C++ implementation from llama.cpp though JNI.
model-integration/src/main/java/ai/vespa/llm/clients/JsonSchemaToGrammar.java