Integrating External JSON APIs
@Generable lets you define a single Swift model and reuse it with Foundation Models and external providers that accept JSON Schema. It saves you from writing custom JSON schemas and parsing logic for each provider, especially as you plan to support backup providers when the Foundation Model is not available.
The @Generable macro can also help you decode JSON data that you can reuse with external providers, not just Apple’s on-device models.
However, it requires iOS 26 or later. In practice, you can use the same structures and enumerations for both Foundation Models and external providers as a fallback when Apple Intelligence is not available—for example, on older devices or in unsupported regions.
Most mainstream APIs (OpenAI, Anthropic, Google) accept JSON Schema for tools/function calling or constrained JSON output. Your @Generable structures automatically generate those schemas, so you keep one source of truth.
Prerequisites and Context
This chapter builds on the structured generation concepts from earlier chapters and tool patterns from the previous chapters. You should understand how @Generable types work, how to create structured schemas, and basic tool calling patterns. This chapter shows how to extend those same patterns to external AI providers when Foundation Models is not available or when you need capabilities beyond the on-device model.
What You Will Learn
By the end of this chapter, you will be able to:
- Reuse
@Generablestructures with external AI providers - Export JSON schemas from your Swift types automatically
- Integrate with OpenAI’s structured output using
response_format - Build fallback systems that work across multiple AI providers
- Handle API integration safely without exposing keys in client code
- Use third-party packages like AIProxy for streamlined integration
Architecture and Streaming
For production apps, avoid exposing provider API keys in client code and use a server or proxy that streams responses to the app. This keeps keys secure and also controls rate limits and retries.
- Server or proxy: Route requests through your backend and stream tokens down to the client UI.
- Local testing: If you call a provider directly from the app, only do so when the user enters their own key in-app, and store it in the Keychain.
This pattern applies to other providers (Anthropic, OpenRouter, Groq, Gemini via OpenAI-compatible endpoints).
Defining the @Generable Structure
Instead of maintaining separate models and parsing logic, you can define your structure once and use it with both Foundation Models and external providers.
@Generable
public struct BedtimeReminderMessage: Codable {
@Guide(description: "Personalized bedtime reminder title (4-8 words max)")
let title: String
@Guide(description: "Engaging reminder message that motivates without being pushy (1-2 sentences)")
let message: String
@Guide(description: "Quick tip for better sleep tonight")
let tip: String
} Including the JSON Schema
When you generate structured output with Foundation Models, the framework automatically includes the schema of your structure as part of the input in a format the model has been trained on. The parameter includeSchemaInPrompt is true by default.
The @Generable type exposes a GenerationSchema representation. You can send this schema to external APIs:
// Get the JSON schema automatically
let schema = BedtimeReminderMessage.generationSchema Printed schema example:
{
"additionalProperties" : false,
"properties" : {
"message" : {
"description" : "Engaging reminder message that motivates without being pushy (1-2 sentences)",
"type" : "string"
},
"tip" : {
"description" : "Quick tip for better sleep tonight",
"type" : "string"
},
"title" : {
"description" : "Personalized bedtime reminder title (4-8 words max)",
"type" : "string"
}
},
"required" : [
"title",
"message",
"tip"
],
"title" : "BedtimeReminderMessage",
"type" : "object",
"x-order" : [
"title",
"message",
"tip"
]
} Making a Request to OpenAI with JSON Schema
You can reuse the exported schema by using the response_format parameter in the OpenAI chat completion request to have the model return a JSON object that conforms to the schema. When response_format is supplied with strict: true, the model output will conform to the supplied schema.
let schema = BedtimeReminderMessage.generationSchema
let schemaJSON = String(
data: try JSONEncoder().encode(schema),
encoding: .utf8
) ?? "{}" Here is a playground example that uses OpenAISession to make a request to the OpenAI API and return the response as a BedtimeReminderMessage:
#Playground {
let instructions = """
You are a personalized sleep coach generating encouraging bedtime reminders.
Context considerations:
- High sleep debt: More urgent, emphasize recovery importance
- Important tomorrow: Focus on performance preparation
- Poor consistency: Encourage routine building
- Stress/caffeine: Acknowledge need for wind-down time
- Workout recovery: Emphasize rest for muscle repair
Create friendly, motivating messages that:
- Are encouraging but never pushy or guilt-inducing
- Reference specific context when helpful
- Provide actionable next steps
- Keep it concise and warm
Match the tone to be supportive and understanding of real-world constraints.
"""
let mockPrompt = """
Generate a bedtime reminder for Rudrank:
Optimal bedtime: 10:45 PM
Sleep need: 7.8h
Sleep debt: 1.3h
Tomorrow importance: high
Recent consistency: 78%
Today's factors: evening workout, 2 coffees after 4 PM, late screen time
Reasoning: Slept below target for the past 3 nights; earlier lights-out will help reduce debt before an early start tomorrow.
"""
let session = LanguageModelSession(instructions: instructions)
let response = try await session.respond(
to: mockPrompt,
generating: BedtimeReminderMessage.self
)
debugPrint(response.content)
let openAISession = OpenAISession(instructions: instructions)
let gptReminder = try await openAISession.respond(
to: mockPrompt,
generating: BedtimeReminderMessage.self
)
debugPrint(gptReminder)
} Here is the example output from Foundation Models and GPT-4o mini:
// Foundation Models
BedtimeReminderMessage(
title: "Prioritize Rest for Tomorrow's Success",
message: "With just 1.3 hours of sleep debt, it is crucial to focus on recovery tonight. Aim for bed by 10:45 PM to tackle tomorrow's tasks with your best self.",
tip: "Avoid caffeine after 4 PM and dim the lights to signal your body it is time to wind down."
)
// GPT-4o mini
BedtimeReminderMessage(
title: "Restful Night for a Big Day Tomorrow",
message: "Hey Rudrank, it is time to start winding down after a busy day. Tonight's sleep sets you up for tomorrow's important events, so aiming for an earlier bedtime will help balance that sleep debt and support recovery from your workout.",
tip: "Consider a calming tea to ease caffeine effects.") Raw URLSession with response_format json_schema
Here is an example of OpenAISession that posts to chat/completions with response_format: json_schema and returns your @Generable type directly:
import Foundation
import FoundationModels
struct OpenAIResponse: Codable {
let choices: [Choice]
struct Choice: Codable {
let message: Message
struct Message: Codable {
let content: String
}
}
}
enum OpenAIError: Error, LocalizedError {
case invalidURL
case invalidResponse
case apiError(statusCode: Int)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .invalidResponse: return "Invalid response"
case .apiError(let statusCode): return "API error with status code: (statusCode)"
}
}
}
final class OpenAISession {
// Replace with a user-provided or proxy key in practice
private let apiKey = "<USER_OR_PROXY_KEY>"
private let baseURL = "https://api.openai.com/v1/chat/completions"
private let instructions: String?
public init(instructions: String? = nil) {
self.instructions = instructions
}
func respond<Content>(
to prompt: String,
generating type: Content.Type = Content.self
) async throws -> Content where Content: Generable {
guard let url = URL(string: baseURL) else {
throw OpenAIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Bearer (apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
// Convert the schema to JSON-compatible format
let schemaData = try JSONEncoder().encode(type.generationSchema)
let schemaJSON = try JSONSerialization.jsonObject(with: schemaData, options: [])
var jsonBody: [String: Any] = [
"model": "gpt-4o-2024-08-06",
"messages": [
["role": "system", "content": instructions ?? ""],
["role": "user", "content": prompt]
]
]
jsonBody["response_format"] = [
"type": "json_schema",
"json_schema": [
"name": "schema",
"strict": true,
"schema": schemaJSON
]
]
let data = try JSONSerialization.data(withJSONObject: jsonBody, options: [])
request.httpBody = data
let (dataOut, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw OpenAIError.invalidResponse
}
guard httpResponse.statusCode == 200 else {
let errorBody = String(data: dataOut, encoding: .utf8) ?? "Unknown error"
print("API Error ((httpResponse.statusCode)): (errorBody)")
throw OpenAIError.apiError(statusCode: httpResponse.statusCode)
}
let openAIResponse = try JSONDecoder().decode(OpenAIResponse.self, from: dataOut)
let json = openAIResponse.choices.first?.message.content ?? "{}"
return try type.init(.init(json: json))
}
} This method demonstrates how to make a request to the OpenAI API and return a structured response. Use this approach on your backend, not the client side, to avoid exposing your API keys.
If your app allows users to enter their own API key, you can use this method on the client side.
Using AIProxy or Other Packages
You can use AIProxy or other packages to make a request to the OpenAI API and return the response in the BedtimeReminderMessage type. The main difference is how each library expects the schema; all enforce strict schema adherence.
I wrote an Encodable schema extension for AIProxy that you can use if you prefer writing everything in Swift.
https://github.com/rudrankriyam/AIProxySwift And here is an example of how to use it:
import AIProxySwift
let openAIService = AIProxy.openAIDirectService(unprotectedAPIKey: key)
/* Uncomment for all other production use cases
// let openAIService = AIProxy.openAIService(
// partialKey: "partial-key-from-your-developer-dashboard",
// serviceURL: "service-url-from-your-developer-dashboard"
// )
*/
let requestBody = OpenAIChatCompletionRequestBody(
model: "gpt-4o-2024-08-06",
messages: [
.system(content: .text(instructions)),
.user(content: .text(mockPrompt))
],
responseFormat: .encodableJSONSchema(name: "BedtimeReminderMessage", schema: BedtimeReminderMessage.generationSchema, strict: true)
)
let response = try await openAIService.chatCompletionRequest(body: requestBody, secondsToWait: 60)
let json = response.choices.first?.message.content ?? ""
let reminder = try BedtimeReminderMessage(GeneratedContent(json: json))
debugPrint(reminder) This approach lets you use the same structure for both Foundation Models and external providers. It works with any endpoint that supports JSON Schema and is compatible with OpenAI-style endpoints, including Anthropic, Google (Gemini), OpenRouter, Groq, and more.
Nutrition JSON to Foundation Models
In Zenther, I have a barcode feature that fetches nutrition data from OpenFoodFacts (JSON), then combines it with Foundation Models for analysis:
func generateNutritionInsight(for food: OpenFoodFactsFood) async throws -> String {
let session = LanguageModelSession(
instructions: Instructions("""
You are a supportive nutrition coach. Use the provided nutrition as facts.
Keep responses brief (2 sentences). Be encouraging and practical.
""")
)
let prompt = """
Food: (food.foodName)
Per 100g: (Int(food.calories)) kcal, (food.protein) g protein, (food.carbohydrates) g carbs, (food.fat) g fat
Provide one short encouragement and one actionable tip.
"""
do {
let response = try await session.respond(to: prompt)
return response.content
} catch LanguageModelSession.GenerationError.guardrailViolation {
return "I cannot provide an insight for this item. Please try another product."
} catch {
return "Unable to generate an insight right now."
}
} This design keeps the facts grounded in a trusted JSON API, and uses the model for tone, prioritization, and guidance.
What’s Next
@Generable lets you define a single Swift model and reuse it with Foundation Models and external providers that accept JSON Schema. It saves you from writing custom JSON schemas and parsing logic for each provider, especially as you plan to support backup providers when the Foundation Model is not available.
With external API integration patterns established, the next chapter explores dynamic generation schemas for runtime schema construction. While @Generable works great for compile-time structures, dynamic schemas enable complex scenarios like user-configurable forms and varying document types where you cannot know the structure ahead of time.