🎯 JsonCraft

MIT Licence pub package GitHub stars pub points

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