JsonText.java
package org.egothor.methodatlas.ai;
/**
* Utility methods for extracting JSON fragments from free-form text produced by
* AI model responses.
*
* <p>
* Some AI providers may return textual responses that contain additional
* commentary or formatting around the actual JSON payload requested by the
* application. This helper provides a minimal extraction mechanism that
* isolates the first JSON object found within such responses so that it can be
* deserialized safely.
* </p>
*
* <p>
* The implementation performs a depth-counted scan from the first opening brace
* (<code>{</code>) and returns the first <em>balanced</em> object, tracking
* string literals and backslash escapes so that braces appearing inside string
* values are not mistaken for structural delimiters.
* </p>
*
* <p>
* The method is intentionally tolerant of provider-specific output formats and
* is primarily used as a defensive measure to recover valid JSON payloads from
* otherwise well-formed responses.
* </p>
*
* <p>
* This class is a non-instantiable utility holder.
* </p>
*
* @see AiSuggestionException
* @see AiClassSuggestion
*/
public final class JsonText {
/** Opening object delimiter. */
private static final char OPEN_BRACE = '{';
/** Closing object delimiter. */
private static final char CLOSE_BRACE = '}';
/** String-literal delimiter. */
private static final char QUOTE = '"';
/** Escape character within a string literal. */
private static final char BACKSLASH = '\\';
/**
* Prevents instantiation of this utility class.
*/
private JsonText() {
}
/**
* Extracts the first complete JSON object found within a text response.
*
* <p>
* The method locates the first opening brace (<code>{</code>) and scans
* forward, counting brace depth until the matching closing brace is reached,
* returning that balanced substring (inclusive). Braces inside string
* literals are ignored, and backslash escapes within strings are honoured, so
* a value such as <code>"a }"</code> does not prematurely terminate the
* object. When the response contains additional objects or trailing
* commentary after the first, only the first complete object is returned.
* </p>
*
* @param text text returned by the AI model
* @return the first balanced JSON object as text
*
* @throws AiSuggestionException if the input text is empty, contains no
* opening brace, or the first object is never
* closed (unbalanced)
*/
public static String extractFirstJsonObject(String text) throws AiSuggestionException {
if (text == null || text.isBlank()) {
throw new AiSuggestionException("Model returned an empty response");
}
int start = text.indexOf(OPEN_BRACE);
if (start < 0) {
throw new AiSuggestionException("Model response does not contain a JSON object: " + text);
}
int depth = 0;
boolean inString = false;
boolean escaped = false;
for (int i = start; i < text.length(); i++) {
char c = text.charAt(i);
if (inString) {
if (escaped) {
escaped = false;
} else if (c == BACKSLASH) {
escaped = true;
} else if (c == QUOTE) {
inString = false;
}
continue;
}
if (c == QUOTE) {
inString = true;
} else if (c == OPEN_BRACE) {
depth++;
} else if (c == CLOSE_BRACE) {
depth--;
if (depth == 0) {
return text.substring(start, i + 1);
}
}
}
throw new AiSuggestionException("Model response does not contain a JSON object: " + text);
}
}