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.

3 comentários :

Ederson Selvati disse...

Luis

Existe algum meio de usar o marshal/unmarshal em outros clientes (android, .net)? Pois quando criamos um método que retorna um JsonValue quando chega do lado de lá é muito dificil de tratar.

Luís Gustavo Fabbro disse...

Ederson

O post cita no final alguns mecanismos para personalizar a serialização. Nenhum desses métodos atende sua necessidade de simplificação? Como seu cliente android ou .net espera receber tais dados? Esses outros ambientes também possuem mecanismos para serializar/desserializar; não é possível customizar essa processo tb essa ponta?

[]s

Ederson Selvati disse...

Luis

Não é nada muito complexo, é um objeto TProduto como segue

TProduto = class
private

public
property Codigo: string read GetCodigo write SetCodigo
property Descricao: string read GetDescricao write SetDescricao

end;


do lado Android/.NET tenho a mesma classe TProduto com os mesmos atributos e nao sei se existe uma forma de pegar o JSON serializado pelo Delphi e deserializa-lo nas outras plataformas pois o JSON gerado pelo marshal é "personalizado". O Delphi prove as classes proxy mas nao encontrei nada que recebe-se este JSON personalizado e o trata-se. O que estou tendo que fazer é criar um JSONArray como retorno dos métodos Delphi e usando funções nativas do java para ler este tipo de estrutura, funciona, mas o trabalho é maior dos dois lados.

Grato pela ajuda.

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.