Sitemap
Eljay

Eljay

Musician, solopreneur & creator of collideborate.me

Exploring Firebase Genkit

4 min readMar 29, 2026

--

Press enter or click to view image in full size
Photo by julien Tromeur on Unsplash

Imagine the AI is like a helpful but sometimes messy friend. When you ask a question, it might give you a long paragraph of words.

But what if you want the answer in a neat box with clear fields like “Title” and “Artist”? Genkit lets you do exactly that using structured output. It’s like giving your friend a blank form and saying: “Fill this exactly, no extra talking!”

Let’s look at each one clearly.

1. Basic Generation (Just Plain Text)

This is the simplest way, you get back normal text.

resp, err := genkit.Generate(ctx, g,
ai.WithModelName(“ollama/kimi-k2.5:cloud”),
ai.WithPrompt(“Give me songs of Hillsong.”),
)

When to use it? When you just want the AI to talk naturally (like a normal chat). You read the result with resp.Text().

2. Structured Output with WithOutputType (The Manual Way)

If you want the answer to follow a specific shape, use ai.WithOutputType.

First, define your struct:

type Song struct {
Title string `json:”title”`
Artist string `json:”artist”`
}

Then ask like this:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName(“ollama/kimi-k2.5:cloud”),
ai.WithPrompt(“Give me one song from Hillsong.”),
ai.WithOutputType(Song{}), // Tell the AI exactly what shape to return
)

After the call, you must unpack the data yourself:

var song Song
if err := resp.Output(&song); err != nil {
log.Fatal(err)
}
log.Printf("Title: %s by %s", song.Title, song.Artist)

This is like ordering food and then taking it out of the delivery bag yourself. You get the full resp object, so you can check raw text, usage stats, or do extra work if needed.

Tip: Always make your prompt match the struct. Ask for “one song” (singular) if your struct is for a single object. If you say “songs” (plural), the AI may return a list, causing a schema error like “Expected: object, given: array”.

3. Structured Output with GenerateData (The Clean & Recommended Way)

This is the shorter, more modern, and preferred way when you only care about the structured data.

song, resp, err := genkit.GenerateData[Song](ctx, g,
ai.WithModelName(“ollama/kimi-k2.5:cloud”),
ai.WithPrompt(“Give me one song from Hillsong.”),
)
  • song → You get the filled Song struct directly (no need to call Output()).
  • resp → You still get the full response object if you need it (you can ignore it with _).

Full clean example:

package main

import (
"context"
"log"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/ollama"
)

type Song struct {
Title string `json:"title"`
Artist string `json:"artist"`
}

func main() {
ctx := context.Background()

ollamaPlugin := &ollama.Ollama{
ServerAddress: "http://localhost:11434",
Timeout: 60,
}

g := genkit.Init(ctx,
genkit.WithPlugins(ollamaPlugin),
genkit.WithDefaultModel("ollama/kimi-k2.5:cloud"),
)

ollamaPlugin.DefineModel(g,
ollama.ModelDefinition{
Name: "kimi-k2.5:cloud",
Type: "chat",
},
nil,
)

// Clean structured generation
song, _, err := genkit.GenerateData[Song](ctx, g,
ai.WithModelName("ollama/kimi-k2.5:cloud"),
ai.WithPrompt("Give me one song from Hillsong."),
)
if err != nil {
log.Fatal(err)
}

log.Printf("Title: %s\nArtist: %s", song.Title, song.Artist)
}

Rule of thumb: Use GenerateData whenever you want structured data. It’s shorter, safer, and does the heavy lifting for you.

What If the User Wants One Song… or Many Songs? (Flexible Structured Output)

So far we’ve only asked for exactly one song. But what if the user says:

  • “Give me one song from Hillsong.”
  • “Give me 5 songs from Hillsong.”
  • “Give me some popular songs from Hillsong.”

We don’t want the code to break or force the user to say only “one”. We need a flexible way that works for one or many songs.

The Smart Way: Use a Wrapper Struct (Recommended)

Think of it like this: Instead of asking the AI to hand you a single toy, you give it a gift box that can hold one toy or many toys.

Get Eljay’s stories in your inbox

Join Medium for free to get updates from this writer.

Here’s how:

// Song is the basic item
type Song struct {
Title string `json:"title"`
Artist string `json:"artist"`
}

// SongsWrapper is the "gift box" that can hold any number of songs
type SongsWrapper struct {
Songs []Song `json:"songs"` // This is a slice (list) of songs
}

Now you can use the same wrapper whether the user wants 1 song or 20!

Full Example Using GenerateData (Cleanest Way)

package main

import (
"context"
"fmt"
"log"

"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/ollama"
)

type Song struct {
Title string `json:"title"`
Artist string `json:"artist"`
}

type SongsWrapper struct {
Songs []Song `json:"songs"`
}

func main() {
ctx := context.Background()

ollamaPlugin := &ollama.Ollama{
ServerAddress: "http://localhost:11434",
Timeout: 60,
}

g := genkit.Init(ctx,
genkit.WithPlugins(ollamaPlugin),
genkit.WithDefaultModel("ollama/kimi-k2.5:cloud"),
)

ollamaPlugin.DefineModel(g,
ollama.ModelDefinition{
Name: "kimi-k2.5:cloud",
Type: "chat",
},
nil,
)

// This prompt works great for 1 or many songs
prompt := "Give me some popular songs from Hillsong. Return them as a list."

wrapper, _, err := genkit.GenerateData[SongsWrapper](ctx, g,
ai.WithModelName("ollama/kimi-k2.5:cloud"),
ai.WithPrompt(prompt),
)
if err != nil {
log.Fatal(err)
}

// Now you can easily work with the list
fmt.Printf("Found %d songs:\n", len(wrapper.Songs))
for i, s := range wrapper.Songs {
fmt.Printf("%d. %s by %s\n", i+1, s.Title, s.Artist)
}
}

How to Make the Prompt Flexible and Reliable

Good prompts that work for both one and many songs:

  • “Give me some popular songs from Hillsong. Always return them inside a ‘songs’ list.”
  • “Return a list of Hillsong songs. If the user asks for one, still put it in the list.”
  • “Give me between 1 and 10 popular Hillsong songs as a list.”

Why the wrapper works so well:

  • The AI always returns an object ({“songs”: […]}) never a raw array.
  • This avoids the common error: “Expected: object, given: array”
  • You can loop through wrapper.Songs even if there’s only 1 song.

Quick Tips

  • Always put a slice ([]Song) inside a struct when returning lists. Don’t try to return a raw slice directly with GenerateData.
  • Be clear in the prompt: tell the AI to return “a list” or “inside a ‘songs’ field”.
  • You can add more fields later (e.g. Year int, LyricsSnippet string) just update the Song struct.

Hire Me :)

Interested in integrating tools like AI automation, or building smart features for your project? Let me handle it for you!

--

--