LLM API Exercise
By the end of the exercise, you should be able to:
- call an LLM API from Java using
HttpClient, - send a prompt to an LLM,
- inspect the JSON returned by an LLM API,
- represent an API response using Java DTOs,
- use Jackson and
ObjectMapperto convert JSON, - request structured JSON from an LLM,
- convert generated JSON into Java objects,
- validate generated data before using it.
Go to:
https://aistudio.google.com/
Sign in with a Google account.
Google AI Studio lets you experiment with Gemini models and create an API key for the Gemini Developer API.
In Google AI Studio, choose Get API key and create a Gemini API key.
You can also start here:
https://aistudio.google.com/app/apikey
Copy the key and keep it private.
An API key is a secret.
Do not:
- put it directly in Java source code,
- commit it to GitHub,
- send it to other students,
- include it in screenshots.
If you accidentally expose an API key, revoke it and create another one.
Gemini Developer API provides a free tier for supported models, subject to Google’s current rate limits.
For this classroom exercise, use the model specified by your teacher.
Suggested model:
gemini-3.5-flash-lite
This model currently has free-tier access and is more than sufficient for this exercise.
Free-tier availability, model names, and rate limits can change. Follow your teacher’s model choice if it differs.
Also remember: data submitted using the free tier may be used by Google to improve its products. Use only classroom/example data. Do not submit confidential, company, or personal information.
Use:
GEMINI_API_KEY
export GEMINI_API_KEY="your-api-key-here"
$env:GEMINI_API_KEY="your-api-key-here"
If you use IntelliJ, you can alternatively add GEMINI_API_KEY to the
environment variables in your application’s Run Configuration.
String apiKey = System.getenv("GEMINI_API_KEY");
if (apiKey == null || apiKey.isBlank()) {
throw new IllegalStateException(
"GEMINI_API_KEY is not configured"
);
}
Do not do this:
String apiKey = "AIza...";
Gemini exposes a REST API.
For this exercise, the basic endpoint has this form:
https://generativelanguage.googleapis.com/v1beta/models/MODEL:generateContent
Using our model:
https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-flash-lite:generateContent
We will send the API key in the x-goog-api-key HTTP header.
A simple request body looks like this:
{
"contents": [
{
"parts": [
{
"text": "Explain what a REST API is in three sentences."
}
]
}
]
}
Notice the nested data structure:
contents
|
+-- parts
|
+-- text
This should already look familiar if you have previously worked with JSON APIs.
Instead of manually building JSON strings, we can use Java data structures and Jackson:
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> body = Map.of(
"contents", List.of(
Map.of(
"parts", List.of(
Map.of(
"text",
"Explain what a REST API is in three sentences."
)
)
)
)
);
String jsonBody = mapper.writeValueAsString(body);
You will need:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
String model = "gemini-3.5-flash-lite";
String endpoint =
"https://generativelanguage.googleapis.com/v1beta/models/"
+ model
+ ":generateContent";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Content-Type", "application/json")
.header("x-goog-api-key", apiKey)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpClient client = HttpClient.newHttpClient();
HttpResponse<String> response =
client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("HTTP status: " + response.statusCode());
System.out.println(response.body());
A successful request should normally return:
HTTP status: 200
followed by a JSON response.
Do not worry about extracting the answer yet.
That is part of the exercise.
Before starting the timed exercise, verify that:
- your Java application runs,
- Jackson is available,
- Java can read
GEMINI_API_KEY, - you know which Gemini model to use,
- the request returns HTTP status
200, - you can see the raw JSON response.
Start with this prompt:
Explain what a REST API is in three sentences.
Send it to Gemini and print the complete response.
Your program now has this basic architecture:
Java application
|
| HTTP POST
v
Gemini API
|
| JSON
v
Java application
Look at the returned JSON.
- Is the generated answer the complete HTTP response?
- Where in the JSON is the generated text?
- What arrays and objects can you identify?
- What other information does Gemini return?
- What HTTP status code did you receive?
An LLM generates its response based on the input it receives.
Try these prompts one at a time.
Explain recursion.
Explain recursion to a 10-year-old.
Explain recursion to a third-semester computer science student.
Use Java in your explanation.
Explain recursion in exactly three sentences.
Then provide one small Java example.
Compare the responses.
- How much does changing the prompt change the answer?
- Which prompt produced the most useful answer?
- What information made the response more useful?
- Do you think the same prompt will always produce exactly the same answer?
Write a prompt that asks Gemini to explain:
Dependency Injection
The explanation should be aimed at a first-semester computer science student and contain a small Java example.
So far, your application prints the entire JSON response.
That is not particularly useful.
We want something like:
String answer = ...;
System.out.println(answer);
Find the generated text in the JSON.
The response contains nested structures conceptually similar to:
response
|
+-- candidates[]
|
+-- content
|
+-- parts[]
|
+-- text
Create Java records representing the relevant parts of the Gemini response.
You will need to represent:
GeminiResponse
|
+-- List<Candidate>
Candidate
|
+-- Content
Content
|
+-- List<Part>
Part
|
+-- text
For example:
public record Part(
String text
) {
}
Create the remaining records yourself.
Convert the response:
ObjectMapper mapper = new ObjectMapper();
GeminiResponse geminiResponse =
mapper.readValue(
response.body(),
GeminiResponse.class
);
Then navigate through your DTOs and print only the generated text.
The goal is:
HTTP response
|
v
JSON
|
v
ObjectMapper
|
v
GeminiResponse
|
v
generated text
- How is this similar to consuming any other REST API?
- What happens if
candidatesis empty? - Should your program assume that every response contains generated text?
Displaying generated text is useful, but an application often needs data, not prose.
We are going to ask Gemini to create a quiz question.
public record QuizQuestion(
String question,
List<String> answers,
int correctAnswer
) {
}
We want Gemini to produce data with this structure:
{
"question": "What does HTTP status 404 mean?",
"answers": [
"Unauthorized",
"Not Found",
"Internal Server Error",
"Created"
],
"correctAnswer": 1
}
correctAnswer is the zero-based index of the correct answer.
Try asking:
Generate one multiple-choice question about Java.
The question must have exactly four possible answers.
Return only JSON with these properties:
question
answers
correctAnswer
correctAnswer must be the zero-based index of the correct answer.
Do not include Markdown or explanations.
Run it several times.
Does the model always produce exactly what you expect?
Gemini supports structured JSON output.
Add a generationConfig to the request:
{
"contents": [
{
"parts": [
{
"text": "Generate one multiple-choice question about Java."
}
]
}
],
"generationConfig": {
"responseMimeType": "application/json"
}
}
This tells Gemini that the generated response should be JSON.
For production applications, Gemini can also be given a JSON schema describing the exact required structure.
For this exercise, responseMimeType plus your clear prompt is enough
to explore the concept.
After extracting Gemini’s generated text, it should contain JSON.
Convert it:
QuizQuestion quizQuestion =
mapper.readValue(
generatedText,
QuizQuestion.class
);
You now have two JSON conversions:
HTTP response JSON
|
| ObjectMapper
v
GeminiResponse DTO
|
| extract generated text
v
Generated JSON
|
| ObjectMapper
v
QuizQuestion DTO
This distinction is important.
The API response is JSON.
Inside that response, the LLM has generated another JSON document for your application.
Print something similar to:
What does HTTP status 404 mean?
1. Unauthorized
2. Not Found
3. Internal Server Error
4. Created
LLM-generated data should not automatically be trusted.
Validate your QuizQuestion.
Check that:
questionis notnull,questionis not blank,answersis notnull,- there are exactly four answers,
- none of the answers are blank,
correctAnsweris between0and3.
For example:
if (quizQuestion.answers() == null ||
quizQuestion.answers().size() != 4) {
throw new IllegalArgumentException(
"Gemini returned an invalid quiz question"
);
}
Add the remaining validation yourself.
Why should output from an LLM be treated similarly to input received from a user?
Turn the program into a tiny quiz application.
The application should:
- Ask the user for a topic.
- Insert the topic into the prompt.
- Send the prompt to Gemini.
- Convert the API response to
GeminiResponse. - Extract the generated JSON.
- Convert the JSON to
QuizQuestion. - Validate the question.
- Display it.
- Ask the user for an answer.
- Tell the user whether the answer was correct.
Example:
What topic would you like a question about?
> REST APIs
Generating question...
What does HTTP status 404 mean?
1. Unauthorized
2. Not Found
3. Internal Server Error
4. Created
Your answer:
> 2
Correct!
Your final architecture is:
User
|
| topic
v
Java application
|
| prompt + JSON
v
Gemini API
|
| API response JSON
v
GeminiResponse
|
| generated JSON
v
QuizQuestion
|
| validation
v
Java application
|
v
User
Generate an entire quiz.
Create:
public record Quiz(
String topic,
List<QuizQuestion> questions
) {
}
Ask the user for:
Topic:
> Java
Difficulty:
> medium
Number of questions:
> 5
Ask Gemini to generate the quiz as structured JSON.
Then run all questions and calculate the final score.
Example:
Java Quiz
Question 1/5
...
Question 2/5
...
Final score: 4/5
Be prepared to discuss:
- How is calling Gemini similar to calling a normal REST API?
- How is it different?
- Why does the wording of a prompt matter?
- Why is structured output useful?
- Why do we still need DTOs when using an LLM?
- Why should generated data be validated?
- What could happen if an application blindly trusts LLM output?
- Why should an API key never be committed to Git?
- What should the program do if Gemini returns HTTP
429,500, or another error? - When would a traditional API or ordinary Java code be a better solution than an LLM?
You already knew this architecture:
REST API
|
v
JSON
|
v
ObjectMapper
|
v
DTO
Using an LLM extends the idea:
User input
|
v
Prompt
|
v
LLM API
|
v
API response JSON
|
v
Generated data
|
v
Validation
|
v
Java DTO
|
v
Application logic
The HTTP and JSON technologies are familiar.
The important new idea is that an LLM generates its response. Generated output can vary and can be incorrect, so application code must treat it as untrusted data and validate it before use.