11 KiB
API Request Architecture Documentation
Overview
This document explains how to create and handle JSON-based API requests in the Translator application. The architecture has been refactored to use a unified, type-safe template system that ensures consistency and maintainability.
Architecture Components
1. JsonHelper
Location: app/src/main/java/eu/gaudian/translator/utils/JsonHelper.kt
The JsonHelper provides unified JSON parsing, validation, and error handling for all API responses.
Key Methods:
// Parse JSON with comprehensive error handling
fun <T> parseJson(json: String, serializer: KSerializer<T>, serviceName: String): Result<T>
// Validate required fields exist in JSON
fun validateRequiredFields(json: String, requiredFields: List<String>): Boolean
// Extract specific field value
fun extractField(json: String, fieldName: String): String?
// Clean and validate JSON string
fun cleanAndValidateJson(json: String): String
2. ApiRequestHandler
Location: app/src/main/java/eu/gaudian/translator/utils/ApiRequestHandler.kt
The single entry point for all API calls. This is the ONLY component that should call ApiManager.getCompletion().
Key Methods:
// Preferred method - use templates
suspend fun <T> executeRequest(template: ApiRequestTemplate<T>): Result<T>
// Legacy method - deprecated
suspend fun <T> executeRequest(prompt: String, serializer: KSerializer<T>, modelType: ModelType): Result<T>
// Text-only requests
suspend fun executeTextRequest(prompt: String, modelType: ModelType): Result<String>
3. ApiRequestTemplates
Location: app/src/main/java/eu/gaudian/translator/utils/ApiRequestTemplates.kt
Type-safe request templates that define the structure and validation for different API operations.
Creating New JSON API Requests
Step 1: Define Your Response Data Class
Create a serializable data class for your API response:
@kotlinx.serialization.Serializable
data class MyApiResponse(
val requiredField: String,
val optionalField: Int? = null,
val items: List<MyItem> = emptyList()
)
@kotlinx.serialization.Serializable
data class MyItem(
val id: String,
val name: String,
val value: Double
)
Step 2: Create a Request Template
Extend BaseApiRequestTemplate<T> for type-safe requests:
class MyApiRequest(
private val parameter1: String,
private val parameter2: Int,
private val language: String
) : BaseApiRequestTemplate<MyApiResponse>() {
override val responseSerializer = MyApiResponse.serializer()
override val modelType = ModelType.TRANSLATION // Choose appropriate type
override val serviceName = "MyService"
override val requiredFields = listOf("requiredField") // Fields that must exist
init {
promptBuilder.basePrompt = "Perform my API operation with $parameter1 and $parameter2"
addDetail("Language: $language")
addDetail("Parameter 2 value: $parameter2")
withJsonResponse("a JSON object with 'requiredField' (string), 'optionalField' (integer), and 'items' array")
}
}
Step 3: Use the Template in Your Service
class MyService(context: Context) {
private val apiRequestHandler = ApiRequestHandler(ApiManager(context), context)
suspend fun performMyOperation(param1: String, param2: Int, language: String): Result<MyApiResponse> {
return try {
Log.i("MyService", "Performing operation with $param1, $param2 in $language")
val template = MyApiRequest(
parameter1 = param1,
parameter2 = param2,
language = language
)
val result = apiRequestHandler.executeRequest(template)
result.map { response ->
Log.i("MyService", "Successfully completed operation")
response
}.onFailure { exception ->
Log.e("MyService", "Failed to perform operation", exception)
}
} catch (e: Exception) {
Log.e("MyService", "Unexpected error in performMyOperation", e)
Result.failure(e)
}
}
}
Available Model Types
Choose the appropriate ModelType for your request:
ModelType.TRANSLATION- For translation-related requestsModelType.DICTIONARY- For dictionary and vocabulary requestsModelType.VOCABULARY- For vocabulary generation and analysisModelType.EXERCISE- For exercise generationModelType.CORRECTION- For text correction
JSON Response Structure Guidelines
1. Always Define Required Fields
override val requiredFields = listOf("id", "name", "data")
2. Use Clear Field Names
// Good
data class Response(val userId: String, val userName: String, val isActive: Boolean)
// Avoid
data class Response(val uid: String, val nm: String, val active: Boolean)
3. Include Type Information in Prompts
withJsonResponse("a JSON object with 'userId' (string), 'userName' (string), 'isActive' (boolean), and 'createdAt' (timestamp)")
Error Handling Best Practices
1. Always Wrap in Try-Catch
suspend fun myMethod(): Result<MyType> = withContext(Dispatchers.IO) {
try {
// Your logic here
result.map { response ->
// Success handling
response
}.onFailure { exception ->
// Failure logging
Log.e("MyService", "Operation failed", exception)
}
} catch (e: Exception) {
Log.e("MyService", "Unexpected error", e)
Result.failure(e)
}
}
2. Log at Appropriate Levels
Log.i("ServiceName", "Starting operation with parameters: $param1, $param2") // Info level
Log.d("ServiceName", "Generated prompt: $prompt") // Debug level
Log.e("ServiceName", "Operation failed", exception) // Error level
3. Validate Responses
// The template automatically validates required fields
// But you can add custom validation if needed
result.map { response ->
if (response.items.isEmpty()) {
Log.w("MyService", "API returned empty items array")
}
response
}
Testing Your API Requests
1. Unit Test Your Template
@Test
fun `MyApiRequest should build correct prompt`() {
val template = MyApiRequest("test", 42, "English")
val prompt = template.buildPrompt()
assertTrue(prompt.contains("test"))
assertTrue(prompt.contains("42"))
assertTrue(prompt.contains("English"))
assertTrue(prompt.contains("JSON object"))
}
2. Test JSON Parsing
@Test
fun `JsonHelper should parse valid response`() {
val json = """{"requiredField": "value", "optionalField": 123}"""
val result = jsonHelper.validateRequiredFields(json, listOf("requiredField"))
assertTrue(result)
}
3. Integration Test
@Test
fun `end to end API request should work`() = runTest {
// Mock the API manager
every {
apiManager.getCompletion(
prompt = any(),
callback = any(),
modelType = ModelType.TRANSLATION
)
} answers {
val callback = thirdArg<(String?) -> Unit>()
callback("""{"requiredField": "test", "optionalField": 123}""")
}
val template = MyApiRequest("test", 42, "English")
val result = apiRequestHandler.executeRequest(template)
assertTrue(result.isSuccess)
assertEquals("test", result.getOrNull()?.requiredField)
}
Migration from Legacy Code
Before (Legacy):
val prompt = PromptBuilder("Do something")
.addDetail("Parameter: $param")
.withJsonResponse("JSON format")
.build()
apiRequestHandler.executeRequest(prompt, MyResponse.serializer(), ModelType.TRANSLATION)
After (New Architecture):
val template = MyApiRequest(param)
apiRequestHandler.executeRequest(template)
Common Patterns
1. Language-Specific Requests
class LanguageSpecificRequest(
private val text: String,
private val sourceLanguage: String,
private val targetLanguage: String
) : BaseApiRequestTemplate<TranslationResponse>() {
override val serviceName = "TranslationService"
init {
promptBuilder.basePrompt = "Translate from $sourceLanguage to $targetLanguage: '$text'"
withJsonResponse("a JSON object with 'translatedText' containing the translation")
}
}
2. Batch Processing Requests
class BatchProcessingRequest(
private val items: List<String>,
private val operation: String
) : BaseApiRequestTemplate<BatchResponse>() {
override val serviceName = "BatchService"
init {
val itemsJson = Json.encodeToString(items)
promptBuilder.basePrompt = "Perform $operation on these items: $itemsJson"
withJsonResponse("a JSON object with 'results' array containing processed items")
}
}
3. Configuration-Based Requests
class ConfigurableRequest(
private val baseText: String,
private val customInstructions: String,
private val outputFormat: String
) : BaseApiRequestTemplate<ConfigurableResponse>() {
override val serviceName = "ConfigurableService"
init {
promptBuilder.basePrompt = baseText
addDetail(customInstructions)
withJsonResponse(outputFormat)
}
}
Troubleshooting
Common Issues and Solutions
-
"Unresolved reference" errors
- Make sure you're calling the correct method (private methods stay within their class)
- Check imports are correct
-
JSON parsing failures
- Use
jsonHelper.validateRequiredFields()to check structure first - Check that your response data class matches the actual JSON structure
- Look at logs for detailed parsing error messages
- Use
-
Template not working
- Verify all required fields are listed in
requiredFields - Check that the prompt clearly explains the expected JSON format
- Ensure the serializer matches your data class
- Verify all required fields are listed in
-
API request timeouts
- Check network connectivity
- Verify the API manager is properly initialized
- Look at API logs for detailed error information
Best Practices Summary
- Always use templates for new API requests
- Define required fields to catch malformed responses early
- Log comprehensively at appropriate levels
- Handle errors gracefully with proper Result types
- Write tests for both templates and parsing logic
- Keep prompts clear and specific about JSON structure
- Use descriptive service names for better logging
- Validate inputs before making API requests
Future Considerations
When extending the architecture:
- Reuse existing templates when possible
- Follow naming conventions consistently
- Document custom logic in comments
- Add comprehensive tests for new functionality
- Consider backward compatibility when changing existing APIs
This architecture is designed to be easily extensible while maintaining type safety and consistency across all API operations.