Beyond basic types like strings and numbers (last tutorial), JSON Schema in LangChain4j supports complex data structures that are essential for real-world applications. This tutorial explores arrays, enums, and nested objects using Ollama's phi3 model running locally.
Key Advanced Schema Elements
LangChain4j provides several schema elements for complex data:
// Arrays and collections
JsonSchemaElement arraySchema = JsonArraySchema.builder()
.items(itemSchema)
.build();
// Enumerated values
JsonSchemaElement enumSchema = JsonEnumSchema.builder()
.enumValues("ACTIVE",
"INACTIVE",
"PENDING")
.build();
// Nested objects
JsonSchemaElement addressSchema = JsonObjectSchema.builder()
.addStringProperty("street")
.addStringProperty("city")
.build();
Working with Arrays
Arrays are essential when you need to extract multiple items from text. The JsonArraySchema allows you to define arrays of any schema type.
Example: Extracting Multiple Entities
This example demonstrates extracting multiple people from a single text using an array schema. We'll extract a list of people with their names and ages.
package com.logicbig.example;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Person(
String name,
int age) {}
package com.logicbig.example;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.chat.request.ResponseFormatType;
import dev.langchain4j.model.chat.request.json.JsonArraySchema;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.model.chat.request.json.JsonSchema;
import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.ollama.OllamaChatModel;
import java.util.List;
public class PeopleArrayExample {
public static void main(String[] args) {
// Create Ollama ChatModel with phi3
ChatModel chatModel =
OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("phi3:mini-128k")
.logRequests(true)
.logResponses(true)
.build();
// Define schema for a single person
JsonSchemaElement personSchema = JsonObjectSchema.builder()
.addStringProperty("name")
.addIntegerProperty("age")
.required("name", "age")
.build();
// Create array schema for multiple people
JsonSchemaElement peopleArraySchema = JsonArraySchema.builder()
.items(personSchema)
.build();
// Create JSON Schema with array as root
JsonSchema jsonSchema = JsonSchema.builder()
.name("People")
.rootElement(peopleArraySchema)
.build();
// Create response format
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormatType.JSON)
.jsonSchema(jsonSchema)
.build();
// Text containing multiple people
String text = """
In the meeting we had John who is 42 years old,
Sarah who is 35 years old, and Michael who is 28 years old.
Also present was Lisa, aged 31.
""";
UserMessage userMessage = UserMessage.from(text);
// Create chat request
ChatRequest chatRequest = ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build();
// Send request and get response
ChatResponse chatResponse = chatModel.chat(chatRequest);
// Extract JSON response
String jsonResponse = chatResponse.aiMessage().text();
System.out.println("JSON Response: " + jsonResponse);
// Parse JSON to List of Person objects
ObjectMapper mapper = new ObjectMapper();
try {
List<Person> people = mapper.readValue(jsonResponse,
new TypeReference<List<Person>>() {});
System.out.println("\nExtracted People:");
for (Person person : people) {
System.out.println(person);
}
} catch (Exception e) {
System.err.println("Error parsing JSON: " + e.getMessage());
}
}
}
OutputJSON Response: [{"name": "John", "age": 42}, {"name": "Sarah", "age": 35}, {"name":"Michael","age":28}, {"name":"Lisa","age":31}]
Extracted People: Person[name=John, age=42] Person[name=Sarah, age=35] Person[name=Michael, age=28] Person[name=Lisa, age=31]
Understanding Array Schema
The key component is JsonArraySchema which defines what type of items the array contains. In our example:
JsonArraySchema.builder().items(personSchema) creates an array of person objects
- Each person object has
name (string) and age (integer) properties
- The LLM identifies multiple people in the text and returns them as a JSON array
Working with Enums
Enums are useful when you have a fixed set of possible values. The JsonEnumSchema ensures the LLM only returns one of the specified values.
Example: Status Extraction with Enums
This example shows how to extract status information using an enum schema. We'll define specific status values and extract them from text.
package com.logicbig.example;
public record ItemStatus(String itemName,
Status status) {
}
package com.logicbig.example;
public enum Status {
ACTIVE,
INACTIVE,
PENDING,
COMPLETED
}
package com.logicbig.example;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.chat.request.ResponseFormatType;
import dev.langchain4j.model.chat.request.json.*;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.ollama.OllamaChatModel;
import java.util.Arrays;
import java.util.List;
public class StatusEnumExample {
public static void main(String[] args) {
// Create Ollama ChatModel with phi3
ChatModel chatModel =
OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("phi3:mini-128k")
.logRequests(true)
.logResponses(true)
.build();
// Create enum schema for status
JsonSchemaElement statusSchema =
JsonEnumSchema.builder()
.enumValues(Arrays.stream(Status.values())
.map(Enum::name)
.toList())
.description("Current status of the item")
.build();
// Create object schema with status field
JsonSchemaElement itemSchema = JsonObjectSchema.builder()
.addStringProperty("itemName")
.addProperty("status", statusSchema)
.required("itemName", "status")
.build();
JsonSchemaElement itemsArraySchema = JsonArraySchema.builder()
.items(itemSchema)
.description("List of items with their statuses")
.build();
// Create JSON Schema
JsonSchema jsonSchema = JsonSchema.builder()
.name("ItemStatusList")
.rootElement(itemsArraySchema)
.build();
// Create response format
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormatType.JSON)
.jsonSchema(jsonSchema)
.build();
// Text describing status
String text = """
The 'Website Redesign' is currently active and progressing well.
The 'Database Migration' has been completed successfully.
The 'User Authentication' is still pending review.
""";
UserMessage userMessage = UserMessage.from(text);
// Create chat request
ChatRequest chatRequest = ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build();
System.out.println("Extracting status information...");
// Send request and get response
ChatResponse chatResponse = chatModel.chat(chatRequest);
// Extract JSON response
String jsonResponse = chatResponse.aiMessage().text();
System.out.println("JSON Response: " + jsonResponse);
ObjectMapper mapper = new ObjectMapper();
try {
List<ItemStatus> items =
mapper.readValue(jsonResponse,
new TypeReference<List<ItemStatus>>() {});
for (ItemStatus item : items) {
System.out.println(item);
}
} catch (Exception e) {
System.err.println("Error parsing JSON: " + e.getMessage());
}
}
}
OutputExtracting status information... JSON Response: [{"itemName": "Website Redesign", "status": "ACTIVE"}, {"itemName": "Database Migration", "status": "COMPLETED"}, {"itemName":"User Authentication","status": "PENDING"}] ItemStatus[itemName=Website Redesign, status=ACTIVE] ItemStatus[itemName=Database Migration, status=COMPLETED] ItemStatus[itemName=User Authentication, status=PENDING]
Enum Schema Benefits
Using JsonEnumSchema provides several advantages:
- Type safety: Only predefined values are allowed
- Consistency: Ensures uniform responses across different inputs
- Validation: Easy to validate against expected values
- Documentation: Clearly communicates allowed values to the LLM
Complex Nested Structures
Real-world data often requires nested objects. You can combine different schema types to create complex hierarchical structures.
Example: Company with Employees
This example demonstrates a complex nested structure: a company with multiple employees, where each employee has contact information and a role.
package com.logicbig.example;
import java.util.List;
public record Company(
String name,
List<Employee> employees) {}
package com.logicbig.example;
public record Employee(
String name,
String email,
String role) {}
package com.logicbig.example;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.chat.request.ResponseFormatType;
import dev.langchain4j.model.chat.request.json.*;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.ollama.OllamaChatModel;
public class CompanyExample {
public static void main(String[] args) {
// Create Ollama ChatModel with phi3
ChatModel chatModel =
OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("phi3:mini-128k")
.logRequests(true)
.logResponses(true)
.build();
// Create enum schema for roles
JsonSchemaElement roleSchema = JsonEnumSchema.builder()
.enumValues("DEVELOPER", "DESIGNER", "MANAGER", "ANALYST")
.description("Employee role")
.build();
// Create schema for employee
JsonSchemaElement employeeSchema = JsonObjectSchema.builder()
.addStringProperty("name")
.addStringProperty("email")
.addProperty("role", roleSchema)
.required("name", "email", "role")
.build();
// Create array schema for employees
JsonSchemaElement employeesArraySchema = JsonArraySchema.builder()
.items(employeeSchema)
.description("List of company employees")
.build();
// Create company schema with employees array
JsonSchemaElement companySchema = JsonObjectSchema.builder()
.addStringProperty("name", "Company name")
.addProperty("employees", employeesArraySchema)
.required("name", "employees")
.build();
// Create JSON Schema
JsonSchema jsonSchema = JsonSchema.builder()
.name("Company")
.rootElement(companySchema)
.build();
// Create response format
ResponseFormat responseFormat = ResponseFormat.builder()
.type(ResponseFormatType.JSON)
.jsonSchema(jsonSchema)
.build();
// Text describing a company
String text = """
Tech Innovations Inc. is a software company with several employees.
John Smith works as a DEVELOPER with email john@tech.com.
Sarah Johnson is a DESIGNER with email sarah@tech.com.
Mike Brown serves as a MANAGER with email mike@tech.com.
Lisa Wang works as an ANALYST with email lisa@tech.com.
""";
UserMessage userMessage = UserMessage.from(text);
// Create chat request
ChatRequest chatRequest =
ChatRequest.builder()
.responseFormat(responseFormat)
.messages(userMessage)
.build();
System.out.println("Extracting company information...");
// Send request and get response
ChatResponse chatResponse = chatModel.chat(chatRequest);
// Extract JSON response
String jsonResponse = chatResponse.aiMessage().text();
System.out.println("JSON Response: " + jsonResponse);
// Parse JSON to Company object
ObjectMapper mapper = new ObjectMapper();
try {
Company company = mapper.readValue(jsonResponse, Company.class);
System.out.println(company);
System.out.println("Number of Employees: " + company.employees().size());
System.out.println("\nEmployees:");
for (Employee emp : company.employees()) {
System.out.println(emp);
}
} catch (Exception e) {
System.err.println("Error parsing JSON: " + e.getMessage());
System.out.println("\nRaw JSON output demonstrates complex nested structure.");
}
}
}
OutputExtracting company information... JSON Response: { "name": "Tech Innovations Inc.", "employees": [ { "name": "John Smith", "email": "john@tech.com" , "role": "DEVELOPER" } , { "name": "Sarah Johnson", "email": "sarah@tech.com", "role": "DESIGNER" }] }
Company[name=Tech Innovations Inc., employees=[Employee[name=John Smith, email=john@tech.com, role=DEVELOPER], Employee[name=Sarah Johnson, email=sarah@tech.com, role=DESIGNER]]] Number of Employees: 2
Employees: Employee[name=John Smith, email=john@tech.com, role=DEVELOPER] Employee[name=Sarah Johnson, email=sarah@tech.com, role=DESIGNER]
Nested Schema Structure
The example shows three levels of nesting:
- Company: Top-level object with name and employees array
- Employee: Object with name, email, and role
- Role: Enum with specific job titles
This demonstrates how LangChain4j can handle complex real-world data structures.
Conclusion
In this tutorial, we've explored advanced JSON Schema elements using Ollama's phi3 model. The output demonstrates that the LLM successfully:
- Extracted multiple people into a JSON array with correct structure
- Used enum values properly for status extraction
- Handled complex nested objects with multiple levels
- Returned structured data that maps directly to Java objects
These advanced schema elements enable you to extract complex, real-world data structures from unstructured text with type safety and predictability. Using Ollama locally makes this approach cost-effective and private, ideal for development and testing.
Example ProjectDependencies and Technologies Used: - langchain4j-ollama 1.10.0 (LangChain4j :: Integration :: Ollama)
- jackson-databind 2.18.2 (General data-binding functionality for Jackson: works on core streaming API)
- JDK 17
- Maven 3.9.11
|