🎯 JsonCraft
A powerful and flexible system for dynamic JSON generation using templates with variable interpolation, conditionals, and formatters.
✨ Features
- 🔗 Variable Interpolation: Access nested data with
{{data.field}} - 🎛️ Smart Conditionals: Include/exclude properties based on conditions
- 🔄 Chainable Formatters: Transform data with pipe syntax
- 📦 Type Preservation: Maintains original types (arrays, objects, numbers)
- 🚫 Negation: Support for inverted conditions with
! - 🛡️ Robust Handling: Gracefully handles null and non-existent values
- 🏗️ Extensible Architecture: Plugin-based formatter system
- 💬 Comments: Document templates with
{{! comment }}syntax - 🔁 Dot Notation: Implicit iterator for primitive arrays with
{{.}} - 🔄 Context Change: Simplify templates with
{{#with:path}} - 🗺️ Map Function: Iterate over arrays to generate dynamic objects
- 📝 Template Inclusion: Modularize templates with
{{#include:id}} - 🎯 Dynamic Partials: Data-driven template selection with
{{#include:*path}}
🚀 Installation
import 'lib/json_craft.dart';
final processor = JsonCraft();
final result = processor.process(jsonTemplate, data);
📖 Usage Guide
1. 🔗 Basic Interpolation
// Template
{
"name": "{{data.usuario.nome}}",
"email": "{{data.usuario.email}}",
"age": "{{data.usuario.idade}}"
}
// Dados
{
"data": {
"usuario": {
"nome": "João Silva",
"email": "joao@email.com",
"idade": 30
}
}
}
// Resultado
{
"name": "João Silva",
"email": "joao@email.com",
"age": 30
}
2. 🎛️ Conditionals
Use {{#if:field}} to conditionally include properties:
// Template
{
"name": "{{data.nome}}",
"{{#if:data.isVip}}vipBenefits": ["Free shipping", "Special discount"],
"{{#if:data.temProdutos}}products": "{{data.produtos}}",
"{{#if:!data.carrinhoVazio}}cartItems": "{{data.carrinho}}"
}
// Dados
{
"data": {
"nome": "Ana",
"isVip": true,
"temProdutos": false,
"carrinhoVazio": false,
"carrinho": ["item1", "item2"]
}
}
// Resultado
{
"name": "Ana",
"vipBenefits": ["Free shipping", "Special discount"],
"cartItems": ["item1", "item2"]
}
🔍 Conditional Evaluation
| Value | {{#if:field}} |
{{#if:!field}} |
|---|---|---|
true |
✅ Included | ❌ Excluded |
false |
❌ Excluded | ✅ Included |
"" (empty string) |
❌ Excluded | ✅ Included |
[] (empty array) |
❌ Excluded | ✅ Included |
{} (empty object) |
❌ Excluded | ✅ Included |
null |
❌ Excluded | ✅ Included |
0 |
❌ Excluded | ✅ Included |
"text" |
✅ Included | ❌ Excluded |
[1,2,3] |
✅ Included | ❌ Excluded |
{"key":"value"} |
✅ Included | ❌ Excluded |
3. 🔄 Formatters
Use the pipe | syntax to apply formatters:
// Template
{
"formattedName": "{{data.nome | titleCase}}",
"username": "{{data.nome | lowerCase | snakeCase}}",
"summary": "{{data.descricao | truncate:50}}"
}
// Dados
{
"data": {
"nome": "joão silva santos",
"descricao": "This is a very long description that needs to be truncated..."
}
}
// Resultado
{
"formattedName": "João Silva Santos",
"username": "joão_silva_santos",
"summary": "This is a very long description that needs to be tr..."
}
📋 Available Formatters
🔤 Formatadores de Caso
| Formatter | Input | Output | Description |
|---|---|---|---|
pascalCase |
"joão silva" | "JoãoSilva" | PascalCase for classes |
camelCase |
"joão silva" | "joãoSilva" | camelCase for variables |
snakeCase |
"João Silva" | "joão_silva" | snake_case for APIs |
kebabCase |
"João Silva" | "joão-silva" | kebab-case for URLs |
titleCase |
"joão silva" | "João Silva" | Title Case for display |
sentenceCase |
"JOÃO SILVA" | "João silva" | Sentence case |
upperCase |
"joão" | "JOÃO" | UPPERCASE |
lowerCase |
"JOÃO" | "joão" | lowercase |
replace(name:data.name) |
"Bem vindo {name}" | "Bem vindo João Silva" | Value substitution |
✏️ Text Formatters
| Formatter | Input | Output | Description |
|---|---|---|---|
capitalize |
"joão silva" | "João silva" | Capitalizes the first letter |
truncate |
"long text..." | "long tex..." | Truncates to 100 chars (default) |
truncate:30 |
"long text..." | "long tex..." | Truncates to 30 chars |
4. 🔗 Chaining Formatters
Combine multiple formatters in sequence:
// Template
{
"processed": "{{data.texto | lowerCase | titleCase | truncate:20}}"
}
// Dados
{
"data": {
"texto": "ESTE É UM TEXTO MUITO LONGO PARA DEMONSTRAÇÃO"
}
}
// Resultado
{
"processed": "Este É Um Texto Muit..."
}
5. 📦 Type Preservation
// Template
{
"originalProducts": "{{data.produtos}}", // Keeps array
"formattedProducts": "{{data.produtos | upperCase}}", // Becomes string
"originalAge": "{{data.idade}}", // Keeps number
"formattedAge": "{{data.idade | upperCase}}" // Becomes string
}
6. 🏗️ Complete Example
import 'dart:convert';
import 'lib/json_craft.dart';
void main() {
final template = '''
{
"usuario": {
"nome": "{{data.usuario.nomeCompleto | titleCase}}",
"username": "{{data.usuario.nomeCompleto | lowerCase | snakeCase}}",
"{{#if:data.usuario.isAdmin}}permissoes": "{{data.usuario.permissoes | upperCase}}"
},
"{{#if:data.produtos}}carrinho": {
"total": "{{data.produtos.length}}",
"primeiroProduto": "{{data.produtos.0.nome | titleCase}}",
"resumo": "{{data.produtos.0.descricao | truncate:50}}"
},
"{{#if:!data.carrinhoVazio}}mensagem": "Carrinho vazio",
"configuracoes": {
"tema": "{{data.tema | capitalize}}",
"idioma": "{{data.idioma | upperCase}}"
}
}
''';
final data = {
"data": {
"usuario": {
"nomeCompleto": "maria silva santos",
"isAdmin": true,
"permissoes": "read write delete"
},
"produtos": [
{
"nome": "notebook gamer",
"descricao": "Notebook para jogos com alta performance e qualidade excepcional"
}
],
"carrinhoVazio": false,
"tema": "dark",
"idioma": "pt-br"
}
};
final processor = JsonCraft();
final result = processor.process(template, data);
print(JsonEncoder.withIndent(' ').convert(json.decode(result)));
}
Result:
{
"usuario": {
"nome": "Maria Silva Santos",
"username": "maria_silva_santos",
"permissoes": "READ WRITE DELETE"
},
"cart": {
"total": 1,
"firstProduct": "Notebook Gamer",
"summary": "Notebook para jogos com alta performance e qualid..."
},
"settings": {
"theme": "Dark",
"language": "PT-BR"
}
}
7. 💬 Comments
Use {{! comment }} to add comments to your templates that will be ignored during processing:
// Template
{
{{! This is a single-line comment }}
"name": "{{data.name}}",
{{!
This is a multi-line comment
that can span multiple lines
and will be completely removed
}}
"email": "{{data.email}}"
}
// Data
{
"data": {
"name": "John Doe",
"email": "john@example.com"
}
}
// Result
{
"name": "John Doe",
"email": "john@example.com"
}
📝 Comment Features
- Single-line comments:
{{! This is a comment }} - Multi-line comments: Support comments that span multiple lines
- Documentation: Great for documenting complex templates
- Clean output: Comments are completely removed before processing
8. 🔁 Dot Notation (Implicit Iterator)
Use {{.}} to access the current item when iterating over arrays of primitives:
// Template
{
"{{#map:data.tags}}tagList": {
"value": "{{.}}",
"uppercase": "{{. | upperCase}}"
}
}
// Data
{
"data": {
"tags": ["javascript", "dart", "flutter"]
}
}
// Result
{
"tagList": [
{"value": "javascript", "uppercase": "JAVASCRIPT"},
{"value": "dart", "uppercase": "DART"},
{"value": "flutter", "uppercase": "FLUTTER"}
]
}
🔍 Dot Notation Features
- Primitive arrays: Works with arrays of strings, numbers, or booleans
- Formatters: Apply formatters to primitive values:
{{. | upperCase}} - Backward compatible: Existing
{{item.property}}syntax still works for objects - Type preservation: When used as complete placeholder, preserves number types
9. 🔄 Context Change (With)
Use {{#with:path}} to change the context and avoid repeating long paths:
// Template WITHOUT context change (repetitive)
{
"userName": "{{data.user.name}}",
"userEmail": "{{data.user.email}}",
"userAge": "{{data.user.age}}",
"userCity": "{{data.user.address.city}}"
}
// Template WITH context change (clean)
{
"{{#with:data.user}}profile": {
"userName": "{{name}}",
"userEmail": "{{email}}",
"userAge": "{{age}}",
"userCity": "{{address.city}}"
}
}
// Data
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com",
"age": 30,
"address": {
"city": "São Paulo"
}
}
}
}
// Result
{
"profile": {
"userName": "John Doe",
"userEmail": "john@example.com",
"userAge": 30,
"userCity": "São Paulo"
}
}
🎯 Context Change Features
- Cleaner templates: Avoid repeating long paths
- Nested contexts: Support for
{{#with}}inside another{{#with}} - Parent context access: Fields not found in new context fall back to parent
- Works with formatters: Apply formatters within the new context
- Combines with other functions: Use with
{{#if}},{{#map}}, etc.
Example: Nested Context
{
"{{#with:data.company}}companyInfo": {
"name": "{{name}}",
"{{#with:employees.manager}}manager": {
"name": "{{name}}",
"{{#with:contact}}contact": {
"email": "{{email}}",
"phone": "{{phone}}"
}
}
}
}
10. 🔄 Map
Use {{#map:campo}} para iterar sobre arrays e gerar objetos dinâmicos:
// Template
{
"{{#map:data.usuarios}}usuarios": {
"titulo": "{{translate.bemVindo}} {{item.nome}} - {{item.idade}}"
}
}
// Dados
{
"translate": {"bemVindo": "Bem vindo"},
"data": {
"usuarios": [
{"nome": "Rafael", "idade": 32},
{"nome": "Ana", "idade": 35}
]
}
}
// Resultado
{
"usuarios": [
{"titulo": "Bem vindo Rafael - 32"},
{"titulo": "Bem vindo Ana - 35"}
]
}
🔍 Avaliação de Map
| Valor | {{#map:campo}} |
|---|---|
[] (array vazio) |
❌ Exclui |
[1,2,3] |
✅ Itera |
O map permite criar objetos dinâmicos baseados em arrays, com suporte a interpolação e formatadores.
8. 🎯 Dynamic Partials
Dynamic Partials allow you to choose which template to include based on data, making your templates truly data-driven!
Static Include (nome fixo)
{
"content": "{{#include:userTemplate}}" // Always uses "userTemplate"
}
Dynamic Include (nome vem dos dados)
// Template
{
"card": "{{#include:*data.cardType}}" // * indicates dynamic
}
// Data - Scenario 1
{
"data": {
"cardType": "userTemplate",
"name": "John"
}
}
// Data - Scenario 2
{
"data": {
"cardType": "adminTemplate",
"name": "Alice"
}
}
🔥 Real-World Use Cases
1. Multi-tenancy / White Label
// Single template for all clients
{
"branding": "{{#include:*client.themeTemplate}}"
}
// Each client can have different template
// Client A: themeTemplate = "clientA_theme"
// Client B: themeTemplate = "clientB_theme"
2. Dynamic Components
{
"{{#map:data.widgets}}widgets": {
"widget": "{{#include:*item.type}}"
}
}
// Each widget uses its own template based on type
// buttonWidget, textWidget, imageWidget, etc.
3. Dynamic Forms
{
"{{#map:data.fields}}formFields": {
"field": "{{#include:*item.fieldType}}"
}
}
// Different field types: inputField, selectField, checkboxField
🎯 Benefits
- ✅ Data-driven: Template selection based on data
- ✅ Zero conditionals: No need for multiple
{{#if}}statements - ✅ Scalable: Add new templates without changing main template
- ✅ Flexible: Works with
{{#map}},{{#with}}, and all other features
9. 📦 Template Inclusion (Static)
Agora é possível incluir templates adicionais no processamento usando o placeholder especial {{#include:id}}. Isso permite modularizar e reutilizar partes do JSON.
Exemplo de Uso
// Template principal
{
"titulo": "{{titulo}}",
"conteudo": "{{#include:subTemplate}}"
}
// Template adicional
{
"subtitulo": "{{subtitulo}}",
"detalhes": "{{detalhes}}"
}
// Dados
{
"titulo": "Título Principal",
"subtitulo": "Subtítulo",
"detalhes": "Alguns detalhes aqui."
}
// Resultado
{
"titulo": "Título Principal",
"conteudo": {
"subtitulo": "Subtítulo",
"detalhes": "Alguns detalhes aqui."
}
}
Como Usar
Passe os templates adicionais como um mapa no método process:
final mainTemplate = json.encode({
'titulo': '{{titulo}}',
'conteudo': '{{#include:subTemplate}}'
});
final subTemplate = json.encode({
'subtitulo': '{{subtitulo}}',
'detalhes': '{{detalhes}}'
});
final templates = {
'subTemplate': subTemplate
};
final data = {
'titulo': 'Título Principal',
'subtitulo': 'Subtítulo',
'detalhes': 'Alguns detalhes aqui.'
};
final resultado = JsonCraft().process(mainTemplate, data, templates: templates);
print(resultado);
Tratamento de Erros
- Template ausente: Lança exceção se o template referenciado não for encontrado.
- Placeholder inválido: Lança exceção para sintaxe incorreta.
🎯 Casos de Uso
🏷️ Geração de Identificadores
{
"className": "{{data.nome | pascalCase}}",
"variableName": "{{data.nome | camelCase}}",
"apiEndpoint": "/{{data.nome | kebabCase}}",
"dbField": "{{data.nome | snakeCase}}"
}
📄 Formatação de Conteúdo
{
"titulo": "{{data.artigo.titulo | titleCase}}",
"resumo": "{{data.artigo.conteudo | truncate:150}}",
"autor": "{{data.artigo.autor | titleCase}}",
"tags": "{{data.artigo.tags | upperCase}}"
}
🔐 Configurações Condicionais
{
"{{#if:data.usuario.isPremium}}features": ["feature1", "feature2"],
"{{#if:!data.usuario.isGuest}}profile": {
"name": "{{data.usuario.nome | titleCase}}",
"settings": "{{data.configuracoes}}"
}
}
🏗️ Arquitetura Extensível
Formatadores Customizados
Você pode criar seus próprios formatadores:
import 'lib/json_craft.dart';
import 'lib/json_craft_formatter.dart';
// Criar formatador customizado
final customFormatter = JsonCraftFormatter(
name: 'reverse',
formatter: (value, param) => value.split('').reversed.join(),
);
// Usar com formatadores customizados
final processor = JsonCraft(formatters: [customFormatter]);
final resultado = processor.process('{"reversed": "{{data.text | reverse}}"}', data);
Sistema de Plugins
A arquitetura baseada em JsonCraftFormatter permite:
- ✅ Formatadores customizados
- ✅ Extensibilidade fácil
- ✅ Reutilização de código
- ✅ Testes isolados
- ✅ Manutenibilidade
🛡️ Tratamento de Erros
O sistema trata graciosamente:
- Campos inexistentes: Lança exceção com detalhes
- Índices inválidos: Lança exceção para arrays
- Formatadores inexistentes: Retorna valor original
- Valores nulos: Retorna string vazia
- Condicionais inválidas: Retorna
false
🧪 Tests
Run tests to verify all functionalities:
flutter test
Current coverage: 55 tests passing ✅
- Basic and nested interpolation
- Conditionals and negation
- All formatters
- Formatter chaining
- Type preservation
- Edge cases and error handling
- Extensible formatter architecture
- Dot notation with primitive arrays
- Comments (single-line and multi-line)
- Context change with nested contexts
- Map function with arrays
- Template inclusion (static and dynamic)
📝 Licença
Este projeto está sob a licença MIT.
Criado com ❤️ usando Flutter e Dart