Feature Tutorials
Structured Outputs & JSON Schemas
Enforce deterministic JSON output from LLMs with Generic ToSchema derivation and the SchemaBuilder DSL.
Why Structured Outputs?
When LLMs generate unconstrained text, extracting data requires fragile regular expressions and unpredictable JSON parsing. With ollama-haskell, you can instruct the model to constrain its generation to a strict JSON Schema, guaranteeing that the model output decodes directly into your Haskell data structures.
Method 1: Automatic Generic Derivation (ToSchema)
The easiest way to enforce structured outputs is by deriving ToSchema via GHC.Generics:
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Data.Aeson (FromJSON, eitherDecode)
import Data.ByteString.Lazy qualified as BSL
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import GHC.Generics (Generic)
import Ollama
-- 1. Define your Haskell record
data CityWeather = CityWeather
{ city :: Text
, temperature :: Double
, conditions :: Text
, humidity :: Maybe Int -- Optional field: omitted from 'required' array
} deriving stock (Generic, Show)
deriving anyclass (ToSchema, FromJSON)
main :: IO ()
main = do
client <- defaultClient
-- 2. Pass 'formatFor @CityWeather' in the chat request
let prompt = userMessage "Give me the weather report for Tokyo right now." :| []
req = (chatRequest "qwen3.5:2b" prompt)
{ chatFormat = Just (formatFor @CityWeather) }
res <- chat client req
case res of
Left err -> putStrLn $ "Error: " <> show err
Right resp -> case crMessage resp of
Just msg -> do
let rawJson = messageContent msg
putStrLn $ "Raw JSON from LLM: " <> show rawJson
-- 3. Decode JSON directly into your strongly typed Haskell data
case eitherDecode (BSL.fromStrict (TE.encodeUtf8 rawJson)) of
Left decodeErr -> putStrLn $ "JSON Parse Error: " <> decodeErr
Right weather -> do
putStrLn "Successfully decoded Haskell record:"
print (weather :: CityWeather)
Nothing -> putStrLn "No message returned"Supported Generic Schema Types
| Haskell Type | JSON Schema Representation | Note |
|---|---|---|
Text, String |
{"type": "string"} |
Primitive string |
Int, Word, Int64 |
{"type": "integer"} |
Integral numbers |
Double, Float |
{"type": "number"} |
Floating point |
Bool |
{"type": "boolean"} |
True/False |
Maybe a |
Inner type a |
Omitted from required properties list |
[a] |
{"type": "array", "items": ...} |
List of items |
| Record types | {"type": "object", "properties": ...} |
Nested objects |
| Nullary Sum types | {"type": "string", "enum": [...]} |
String enums |
Method 2: Manual SchemaBuilder DSL
For dynamic schemas or when you do not wish to define a dedicated Haskell record type, use the SchemaBuilder DSL (available directly from Ollama):
import Ollama
personSchema :: Schema
personSchema = buildSchema $ emptyObject
|+ ("name", JString)
|+ ("age", JInteger)
|+ ("skills", JArray JString)
|! "name" -- Mark 'name' as required
|! "skills" -- Mark 'skills' as required
main :: IO ()
main = do
client <- defaultClient
let req = (generateRequest "qwen3.5:2b" "Extract person details.")
{ genFormat = Just (SchemaFormat personSchema) }
res <- generate client req
...