Ollama has recently introduced native support for JSON structured output, as described in https://ollama.com/blog/structured-outputs.

This PR introduces support for it, both for directly passing a JSON schema and when using the Spring AI output conversion APIs.

Example

Data:

record CountryInfo(
        @JsonProperty(required = true) String name, 
        @JsonProperty(required = true) String capital, 
        @JsonProperty(required = true) List<String> languages
) {}

Using BeanOutputConverter

Example with ChatClient:

@PostMapping("/chat/json")
CountryInfo chatJsonOutput(String country) {
    var outputConverter = new BeanOutputConverter<>(CountryInfo.class);
    var userPromptTemplate = """
            Tell me about {country}.
            """;

    return chatClient.prompt()
            .user(userSpec -> userSpec
                    .text(userPromptTemplate)
                    .param("country", country)
            )
            .options(OllamaOptions.builder()
                    .withFormat(outputConverter.getJsonSchemaMap())
                    .build())
            .call()
            .entity(outputConverter);
}

Example with ChatModel:

@GetMapping("/chat/json")
CountryInfo chatJsonOutput(String country) {
    var outputConverter = new BeanOutputConverter<>(CountryInfo.class);
    var userPromptTemplate = new PromptTemplate("""
            Tell me about {country}.
            """);
    Map<String,Object> model = Map.of("country", country);
    var prompt = userPromptTemplate.create(model, OllamaOptions.builder()
            .withModel(OllamaModel.LLAMA3_2.getName())
            .withFormat(outputConverter.getJsonSchemaMap())
            .build());

    var chatResponse = chatModel.call(prompt);
    return outputConverter.convert(chatResponse.getResult().getOutput().getText());
}

Using plain JSON Schema

Example with ChatClient:

@PostMapping("/chat/json")
CountryInfo chatJsonOutput(String country) throws JsonProcessingException {
    var userPromptTemplate = """
            Tell me about {country}.
            """;

    var jsonSchema = """
            {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "capital": {
                    "type": "string"
                  },
                  "languages": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "name",
                  "capital",
                  "languages"
                ]
              }
            """;

    return chatClient.prompt()
            .user(userSpec -> userSpec
                    .text(userPromptTemplate)
                    .param("country", country)
            )
            .options(OllamaOptions.builder()
                    .withFormat(new ObjectMapper().readValue(jsonSchema, Map.class))
                    .build())
            .call()
            .entity(CountryInfo.class);
}

Example with ChatModel:

@GetMapping("/chat/json")
CountryInfo chatJsonOutput(String country) throws JsonProcessingException {
    var outputConverter = new BeanOutputConverter<>(CountryInfo.class);
    var userPromptTemplate = new PromptTemplate("""
            Tell me about {country}.
            """);
    Map<String,Object> model = Map.of("country", country);

    var jsonSchema = """
            {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "capital": {
                    "type": "string"
                  },
                  "languages": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "name",
                  "capital",
                  "languages"
                ]
              }
            """;

    var prompt = userPromptTemplate.create(model, OllamaOptions.builder()
            .withModel(OllamaModel.LLAMA3_2.getName())
            .withFormat(new ObjectMapper().readValue(jsonSchema, Map.class))
            .build());

    var chatResponse = chatModel.call(prompt);
    return outputConverter.convert(chatResponse.getResult().getOutput().getText());
}

Comment From: tzolov

Thanks @ThomasVitale

Comment From: tzolov

Rebased and merged at 6ab7e20616a17fbe3e54400658d5c11961b4eb8a