From afd0fea51772b63924d6d1ece5ad59ef03c6e763 Mon Sep 17 00:00:00 2001 From: jnbdz Date: Thu, 6 Aug 2026 13:00:17 -0400 Subject: [PATCH] Fix StringIndexOutOfBoundsException when resolving a self $ref '#' A $ref pointing at the schema itself ('#', or 'id#' after contextual resolution) produces an empty fragment in JsonRef.resolveUri, which then fails on path.charAt(0). Treat an empty fragment the same as no fragment so the ref resolves to the schema identified by the prefix. Fixes #141 --- .../io/vertx/json/schema/impl/JsonRef.java | 4 +- .../java/io/vertx/tests/impl/RefTest.java | 77 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/vertx/json/schema/impl/JsonRef.java b/src/main/java/io/vertx/json/schema/impl/JsonRef.java index f24d2c35..1bc96f06 100644 --- a/src/main/java/io/vertx/json/schema/impl/JsonRef.java +++ b/src/main/java/io/vertx/json/schema/impl/JsonRef.java @@ -320,7 +320,9 @@ private static JsonObject resolveUri(String uri, Map anchors // [prefix, path] final String[] parts = uri.split("#", 2); - final boolean hashPresent = parts.length == 2 && parts[1] != null; + // an empty fragment ("#" or "id#") is equivalent to no fragment at all: the uri refers + // to the schema identified by the prefix, e.g.: the root schema when the prefix is empty + final boolean hashPresent = parts.length == 2 && parts[1] != null && !parts[1].isEmpty(); final String prefix = parts[0]; final String path = hashPresent ? parts[1] : null; diff --git a/src/test/java/io/vertx/tests/impl/RefTest.java b/src/test/java/io/vertx/tests/impl/RefTest.java index 3683c700..f3cc727d 100644 --- a/src/test/java/io/vertx/tests/impl/RefTest.java +++ b/src/test/java/io/vertx/tests/impl/RefTest.java @@ -203,6 +203,83 @@ public void testCase2() { "string"); } + @Test + public void testRefToRoot() { + JsonObject schema = new JsonObject() + .put("type", "object") + .put("properties", new JsonObject() + .put("children", new JsonObject() + .put("type", "array") + .put("items", new JsonObject() + .put("$ref", "#")))); + + JsonObject resolved = JsonRef.resolve(schema); + + JsonObject items = resolved + .getJsonObject("properties") + .getJsonObject("children") + .getJsonObject("items"); + assertEquals("object", items.getString("type")); + // the ref points back to the root schema itself + assertNotNull( + items + .getJsonObject("properties") + .getJsonObject("children") + .getJsonObject("items")); + } + + @Test + public void testRefToRootWithId() { + JsonObject schema = new JsonObject() + .put("$id", "http://www.example.com/tree/") + .put("type", "object") + .put("properties", new JsonObject() + .put("children", new JsonObject() + .put("type", "array") + .put("items", new JsonObject() + .put("$ref", "#")))); + + JsonObject resolved = JsonRef.resolve(schema); + + JsonObject items = resolved + .getJsonObject("properties") + .getJsonObject("children") + .getJsonObject("items"); + assertEquals("object", items.getString("type")); + assertNotNull( + items + .getJsonObject("properties") + .getJsonObject("children") + .getJsonObject("items")); + } + + @Test + public void testRefToRootThroughRepository() { + SchemaRepository repo = + SchemaRepository.create(new JsonSchemaOptions().setBaseUri("http://vertx.io").setDraft(Draft.DRAFT4)); + JsonObject schema = new JsonObject() + .put("type", "object") + .put("properties", new JsonObject() + .put("children", new JsonObject() + .put("type", "array") + .put("items", new JsonObject() + .put("$ref", "#")))); + repo.dereference(JsonSchema.of(schema.copy())); + + JsonObject resolved = repo.resolve(schema); + + JsonObject items = resolved + .getJsonObject("properties") + .getJsonObject("children") + .getJsonObject("items"); + assertEquals("object", items.getString("type")); + assertNotNull( + items + .getJsonObject("properties") + .getJsonObject("children") + .getJsonObject("items")); + } + @Test void testSerialization() { SchemaRepository repo =