29 de novembro de 2013

Convertendo objetos Delphi para o formato JSON

Damos o nome de serialização ao processo de pegar um objeto em memória numa linguagem de programação e convertê-lo para um formato padrão, compreensível para outras linguagens ou ambientes operacionais. A lista de tais formatos inclui sequência de bytes (stream), XML e, mais recentemente, JSON - sendo este último facilmente intercambiável através da internet. Falei sobre esse formato no post Lendo dados JSON em aplicações Delphi, onde mostro como transformar dados JSON em um objeto Delphi.

As classes descritas naquele post podem ser usadas manualmente para realizar o processo inverso, isso é, pegar um objeto no Delphi e representá-lo como um dado JSON. No entanto, o Delphi introduziu classes mais apropriadas para esse serviço, permitindo serializar objetos com facilidade.

Tomemos como exemplo um objeto da classe TWPedido, declarada no quadro abaixo. A classe proposta é complexa, composta tanto de propriedades de tipos atômicos quanto de tipos definidos pelo usuário, como a propriedade Produtos, que é um array dinâmico com instâncias de outra classe (TWProdutoPedido):
TWProdutoPedido = class
public
Codigo : String;
Descr : String;
Qtde: Double;
VrUnitario : Double;
end;

TWPedido = class
public
Numero: Longint;
Data: TDateTime;
VlrTotal: Double;
Produtos: array of TWProdutoPedido;

Constructor Create;
Destructor Destroy;override;

procedure AddProduto(AProd: TWProdutoPedido);
function GetQtdeProdutos : Integer;
end;

{ ... }

Constructor TWPedido.Create;
begin
SetLength (Produtos, 0);
end;

Destructor TWPedido.Destroy;
var lProd: TWProdutoPedido;
begin
for lProd in Produtos do
lProd.Free;

SetLength (Produtos, 0);
inherited;
end;

procedure TWPedido.AddProduto(AProd: TWProdutoPedido);
begin
{ Abre espaço para mais um produto }
SetLength (Produtos, Length (Produtos) + 1);
Produtos [Length (Produtos) - 1] := AProd;

VlrTotal := VlrTotal + (AProd.Qtde * AProd.VrUnitario);
end;

function TWPedido.GetQtdeProdutos : Integer;
begin
Result := Length (Produtos);
end;

function NovoPedido (ANro: Longint) : TWPedido;
var lProd: TWProdutoPedido;
begin
Result := TWPedido.Create;

Result.Numero := ANro;
Result.Data := Now;

lProd := TWProdutoPedido.Create;
lProd.Codigo := 'P001';
lProd.Descr := 'Computador';
lProd.Qtde := 1.000;
lProd.VrUnitario := 1500.00;
Result.AddProduto (lProd);

{ ... }

lProd := TWProdutoPedido.Create;
lProd.Codigo := 'P003';
lProd.Descr := 'Projetor';
lProd.Qtde := 1.000;
lProd.VrUnitario := 745.00;
Result.AddProduto (lProd);
end;

Serializar esse objeto em Delphi passa a ser questão de criarmos uma instância da classe TJSONMarshal. Tal classe se baseará nas informações adicionadas via RTTI (Run Time Type Information) para exportar automaticamente todos os campos públicos do objeto, mesmo estruturas e classes complexas criadas pelo próprio programador. Isso é feito num processo recursivo, de modo que toda a estrutura do objeto é considerada. O código a seguir mostra como usar a TJSONMarshal para fazer a serialização básica de uma instância da classe TWPedido:
var lMarshal : TJSONMarshal;
lPedido: TWPedido;
strJSON : string;
begin
{ Cria um novo pedido }
lPedido := NovoPedido (1234);

{ Realiza a serialização do pedido }
lMarshal := TJSONMarshal.Create (TJSONConverter.Create);

strJSON := lMarshal.Marshal(lPedido).ToString();

lMarshal.Free;
end;

O resultado será um texto no formato JSON semelhante ao publicado a seguir:
{"type":"WJsonObj.TWPedido","id":1,"fields":{"Numero":1234,"Data":41606.7632623727,"VlrTotal":2543,
"Produtos":[{"type":"WJsonObj.TWProdutoPedido","id":2,"fields":{"Codigo":"P001","Descr":"Computador","Qtde":1,"VrUnitario":1500}},

...

{"type":"WJsonObj.TWProdutoPedido","id":4,"fields":{"Codigo":"P003","Descr":"Projetor","Qtde":1,"VrUnitario":745}}]}}

Note que o mecanismo do Delphi inclui no resultado o nome do tipo de dado que originou o texto JSON, uma propriedade fields para armazenar todos os campos do objeto e uma identificação (id) para cada objeto. Esses valores são úteis no processo de transformação do texto JSON de volta para um objeto Delphi, o que pode ser conseguido através da classe TJSONUnMarshal.

O mecanismo possui ainda alguns truques para flexibilizar a serialização. Por exemplo, para impedir determinados campos sejam incluídos no resultado, podemos marcá-los com o atributo JSONMarshalled. Veja uma redeclaração da classe TWPedido solicitando a omissão do campo de valor total:
TWPedido = class
public
Numero: Longint;
Data: TDateTime;

{Marca VlrTotal para não ser incluído na serialização }
[JSONMarshalled(false)]
VlrTotal: Double;

Produtos: array of TWProdutoPedido;

{ ... }
end;

Um recurso mais elaborado consiste em registrar uma função anônima para realizar conversões personalizadas. É possível converter uma classe inteira para um outro tipo de dado mas também é permitido pinçar um único campo e tratá-lo conforme a necessidade. Por exemplo, o campo Data na classe de pedido é, por padrão, serializada como um double. Podemos interceptar sua conversão e forçá-lo a ser serializado como uma data textual:
lMarshal := TJSONMarshal.Create (TJSONConverter.Create);

{ Registra um conversor exclusivo para o campo Data do pedido }
lMarshal.RegisterConverter(TWPedido, 'Data',
function (Data: TObject; Field: string): string
var lProd : TWPedido;
begin
lProd := Data As TWPedido;
Result := DateToStr (lProd.Data);
end
);

strJSON := lMarshal.Marshal(lPedido).ToString();

lMarshal.Free;

Como podemos ver, a função RegisterConverter da classe de serialização deve ser chamada antes de fazermos a serialização em si. Após isso, o conversor que registramos é válido para quaisquer serializações que realizarmos com a instância do TJSONMarshal onde o registro foi feito. Embora o exemplo tenha registrado um único conversor, é permitido registrar tantos conversores quantos forem necessários para o trabalho. Há outras sobrecargas de RegisterConverter para atender diferentes demandas de particularização do processo; a documentação delas pode ser acessada neste link.

Para finalizar, uma última forma de interceptar a serialização é usando o TJSONConverter, cuja instância é passada ao construtor do TJSONMarshal. O TJSONConverter implementa uma série de eventos relativos aos diferentes tipos de dados tratados pela serialização e publica-os como métodos virtuais. Isto possibilita a criação de uma herança do conversor para modificar um ou mais desses eventos e personalizar o tratamento da serialização do respectivo tipo de dado.