Mantenha capitalização ao serializar dicionários

92

Tenho um projeto de API da Web sendo configurado assim:

config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

No entanto, quero que a caixa das chaves do dicionário permaneça inalterada. existe algum atributo em que Newtonsoft.Jsoneu possa usar para uma classe para denotar que desejo que a capitalização permaneça inalterada durante a serialização?

public class SomeViewModel
{
    public Dictionary<string, string> Data { get; set; }    
}
zafeiris.m
fonte
1
Você tentou o resolvedor padrão?
Mateus
1
@Matthew Não, não tenho; você pode explicar com um exemplo como o código ficaria? Observe, ainda quero a serialização da caixa do Camel para todas as minhas solicitações de API da web, só quero a serialização personalizada para uma classe (ou talvez para quaisquer chaves de dicionário).
zafeiris.m

Respostas:

132

Não há um atributo para fazer isso, mas você pode fazer isso personalizando o resolvedor.

Vejo que você já está usando um CamelCasePropertyNamesContractResolver. Se você derivar uma nova classe de resolução dessa classe e substituir o CreateDictionaryContract()método, poderá fornecer uma DictionaryKeyResolverfunção substituta que não altera os nomes das chaves.

Aqui está o código de que você precisa:

class CamelCaseExceptDictionaryKeysResolver : CamelCasePropertyNamesContractResolver
{
    protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
    {
        JsonDictionaryContract contract = base.CreateDictionaryContract(objectType);

        contract.DictionaryKeyResolver = propertyName => propertyName;

        return contract;
    }
}

Demo:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo
        {
            AnIntegerProperty = 42,
            HTMLString = "<html></html>",
            Dictionary = new Dictionary<string, string>
            {
                { "WHIZbang", "1" },
                { "FOO", "2" },
                { "Bar", "3" },
            }
        };

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCaseExceptDictionaryKeysResolver(),
            Formatting = Formatting.Indented
        };

        string json = JsonConvert.SerializeObject(foo, settings);
        Console.WriteLine(json);
    }
}

class Foo
{
    public int AnIntegerProperty { get; set; }
    public string HTMLString { get; set; }
    public Dictionary<string, string> Dictionary { get; set; }
}

Aqui está o resultado do acima. Observe que todos os nomes de propriedade de classe são caseiros, mas as chaves do dicionário mantiveram sua caixa original.

{
  "anIntegerProperty": 42,
  "htmlString": "<html></html>",
  "dictionary": {
    "WHIZbang": "1",
    "FOO": "2",
    "Bar": "3"
  }
}
Brian Rogers
fonte
2
Para sua informação, PropertyNameResolver agora está obsoleto. Parece que contract.DictionaryKeyResolver = key => key;funciona bem.
John Gietzen
1
Isso ainda é MUITO relevante com tipos anônimos, especialmente quando queremos uma caixa de camelo para a maior parte da estrutura, mas não queremos que as chaves dentro de dicionários sejam camelizadas.
Chris Schaller
Concordo absolutamente com Chris. Fui forçado a passar por vários obstáculos no meu JavaScript só porque não consigo evitar que os dicionários sejam camelCased. Acontece que uma linha de código resolverá esse problema (e tornará meu JavaScript muito mais simples)!
Stephen Chung
@BrianRogers Funciona muito bem! No entanto, você sabe se posso condicionar usando meu DictionaryKeyResolversomente se minha propriedade Dicionário tiver algum atributo personalizado?
Mugen
@Mugen Não em cima da minha cabeça. Eu recomendaria fazer isso como uma nova pergunta. Você pode retornar a esta pergunta se precisar fornecer contexto.
Brian Rogers
67

Json.NET 9.0.1 introduziu a NamingStrategyhierarquia de classes para lidar com esse tipo de problema. Ele extrai a lógica para o remapeamento algorítmico de nomes de propriedade do resolvedor de contratos para uma classe leve e separada que permite o controle de chaves de dicionário , nomes de propriedades explicitamente especificados e nomes de dados de extensão (em 10.0.1 ) são remapeados.

Ao usar DefaultContractResolvere definir NamingStrategypara uma instância de, CamelCaseNamingStrategyvocê pode gerar JSON com nomes de propriedade camel-case e chaves de dicionário não modificadas definindo-o em JsonSerializerSettings.ContractResolver:

var resolver = new DefaultContractResolver
{
    NamingStrategy = new CamelCaseNamingStrategy
    {
        ProcessDictionaryKeys = false,
        OverrideSpecifiedNames = true
    }
};
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = resolver;

Notas:

  • A implementação atual de CamelCasePropertyNamesContractResolvertambém especifica que os membros .Net com nomes de propriedades explicitamente especificados (por exemplo, aqueles onde JsonPropertyAttribute.PropertyNamefoi definido) devem ter seus nomes remapeados:

    public CamelCasePropertyNamesContractResolver()
    {
        NamingStrategy = new CamelCaseNamingStrategy
        {
            ProcessDictionaryKeys = true,
            OverrideSpecifiedNames = true
        };
    }

    A resolverdescrição acima preserva esse comportamento. Se você não quer isso, defina OverrideSpecifiedNames = false.

  • Json.NET tem várias estratégias de nomenclatura integradas, incluindo:

    1. CamelCaseNamingStrategy. Uma estratégia de nomenclatura de caixa de camelo que contém a lógica de remapeamento de nome incorporada anteriormente CamelCasePropertyNamesContractResolver.
    2. SnakeCaseNamingStrategy. Uma estratégia de nomenclatura de caso de cobra .
    3. DefaultNamingStrategy. A estratégia de nomenclatura padrão. Os nomes das propriedades e as chaves de dicionário permanecem inalterados.

    Ou você pode criar seu próprio herdando da classe base abstrata NamingStrategy.

  • Embora também seja possível modificar o NamingStrategyde uma instância de CamelCasePropertyNamesContractResolver, uma vez que o último compartilha informações de contrato globalmente em todas as instâncias de cada tipo , isso pode levar a efeitos colaterais inesperados se seu aplicativo tentar usar várias instâncias de CamelCasePropertyNamesContractResolver. Esse problema não existe com DefaultContractResolver, por isso é mais seguro usar quando qualquer customização da lógica de caixa é necessária.

dbc
fonte
Esta solução não está funcionando para uma propriedade como public Dictionary<string, Dictionary<string, string>> Values { get; set; }. Ele ainda faz camelCase para as chaves do dicionário interno.
hikalkan
@hikalkan - embora não tenha conseguido reproduzir exatamente o seu problema, consegui encontrar um problema ao usar várias instâncias de CamelCasePropertyNamesContractResolver. Basicamente, NamingStrategypara o primeiro influenciaria os contratos gerados pelo segundo. Isso pode ser o que você está vendo. Experimente a nova recomendação e me informe se ela corrige seu problema.
dbc
1
Existe um flexível NamingStrategy, de modo que seja capaz de analisar ambas as caixas camel e pascal?
Shimmy Weitzhandler
@dbc No exemplo de código inicial, o que configdeveria ser?
Ryan Lundy
@RyanLundy - Copiei-lo a partir da pergunta inicial, que mostrou a seguinte linha de código: config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();. Parece ser a API da Web MVC 4 HttpConfiguration, consulte Como definir JsonSerializerSettings personalizado para Json.NET na API da Web MVC 4? .
dbc
12

Essa é uma resposta muito boa. Mas por que não simplesmente substituir o ResolveDictionaryKey?

class CamelCaseExceptDictionaryResolver : CamelCasePropertyNamesContractResolver
    {
        #region Overrides of DefaultContractResolver

        protected override string ResolveDictionaryKey(string dictionaryKey)
        {
            return dictionaryKey;
        }

        #endregion
    }
Dmitriy
fonte
Muito conciso. Obrigado por compartilhar.
Abu Abdullah
1

A resposta selecionada é perfeita, mas acho que, no momento em que estou digitando isso, o resolvedor de contratos deve mudar para algo assim porque DictionaryKeyResolver não existe mais :)

public class CamelCaseExceptDictionaryKeysResolver : CamelCasePropertyNamesContractResolver
    {
        protected override JsonDictionaryContract CreateDictionaryContract(Type objectType)
        {
            JsonDictionaryContract contract = base.CreateDictionaryContract(objectType);
            contract.PropertyNameResolver = propertyName => propertyName;
            return contract;
        }
    }
Set
fonte
4
Na verdade, o inverso é verdadeiro. Você deve estar usando uma versão antiga do Json.Net. DictionaryKeyResolverfoi adicionado na versão 7.0.1 e PropertyNameResolverfoi marcado como obsoleto.
Brian Rogers