7

CNPJ alfanumérico chega em julho — sua validação já está preparada?

CNPJ alfanumérico chega em julho — sua validação já está preparada?

A Receita Federal mudou o formato do CNPJ. A partir de julho de 2026, os novos CNPJs vão começar a sair com letras (alfanumérico). O algoritmo dos dígitos verificadores também mudou.

Se seu sistema valida, armazena ou exibe CNPJ, isso vai te afetar mais cedo ou mais tarde.


O que mudou de verdade

O formato antigo continua valendo:

11.222.333/0001-81

Mas os novos podem vir assim:

12.ABC.345/01DE-35

Mesma máscara, mas agora as 12 primeiras posições aceitam letras. E o cálculo dos dígitos verificadores mudou: em vez de só número, agora considera o valor ASCII do caractere (menos 48).

Tem um vetor oficial da própria Receita que vira o santo graal pra testar:

Input: 12ABC34501DE → DV1: 3, DV2: 5 → 12.ABC.345/01DE-35


O problema com as libs atuais

Testei várias libs populares de CNPJ no npm antes de começar a minha. Quase todas quebram com letra:

  • Usam parseInt, Number() ou convertem pra número
  • Regex que só aceita [0-9]
  • Armazenam como number no banco (o que já era ruim e agora vai ser pior)

Resumo: a grande maioria ainda não está preparada pro novo formato.


Como validar corretamente (algoritmo oficial)

Aqui vai a implementação baseada na documentação da Receita/SERPRO:

function calcDv(cnpjBase: string): [number, number] {
  const toValue = (c: string) =>
    /\d/.test(c) ? parseInt(c, 10) : c.charCodeAt(0) - 48

  const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
  const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]

  const chars = cnpjBase.split('')

  const sum1 = chars.reduce((acc, c, i) => acc + toValue(c) * weights1[i], 0)
  const dv1 = sum1 % 11 < 2 ? 0 : 11 - (sum1 % 11)

  const sum2 = [...chars, String(dv1)].reduce(
    (acc, c, i) => acc + toValue(c) * weights2[i], 0
  )
  const dv2 = sum2 % 11 < 2 ? 0 : 11 - (sum2 % 11)

  return [dv1, dv2]
}

Testa com o vetor oficial pra ver se tá certo:

  • 12ABC34501DE35 → deve validar
  • 11.222.333/0001-81 → deve continuar validando

Dica de ouro: se você ainda guarda CNPJ como INT, BIGINT ou CHAR(14) no banco, migra pra VARCHAR(14) o quanto antes.


O que eu fiz

Criei a @br-validators/core exatamente pra resolver isso de forma definitiva.

É uma lib TypeScript open source (MIT), zero dependência, tree-shakeable e que já nasceu com suporte completo ao CNPJ alfanumérico.

npm install @br-validators/core

Uso bem simples:

import { validateCnpj, formatCnpj } from '@br-validators/core/cnpj'

validateCnpj('11.222.333/0001-81')
// { ok: true, value: '11222333000181', format: 'numeric' }

validateCnpj('12ABC34501DE35')
// { ok: true, value: '12ABC34501DE35', format: 'alphanumeric' }

formatCnpj('12ABC34501DE35')
// { ok: true, formatted: '12.ABC.345/01DE-35' }

Ela nunca joga exceção. Sempre retorna { ok: true/false, ... }.

Além do CNPJ, a lib já tem validação/formatação pra 18 documentos brasileiros (CPF, CEP, Placa Mercosul, RENAVAM, Inscrição Estadual, etc) + adapters pro Zod e React Hook Form.


O que você deveria fazer agora (mesmo que não use a lib)

  1. Auditar colunas de CNPJ no banco (migra pra VARCHAR)
  2. Testar seu validador atual com 12ABC34501DE35
  3. Revisar APIs que recebem CNPJ de terceiros

Se quiser contribuir, testar ou só dar feedback, o repo tá aberto:

GitHub: github.com/AlexandreZanata/br-validators

Qualquer sugestão de melhoria na lib é bem-vinda. Sou dev solo do interior do Mato Grosso '-'

Carregando publicação patrocinada...
7

Se me permite algumas sugestões:

Não precisa usar split, dá para percorrer a string em um loop simples e usar charCodeAt(indice) para obter o caractere da posição desejada. E não precisa usar uma expressão regular para ver se cada caractere é um dígito, pois charCodeAt vai funcionar tanto para caracteres que são dígitos quanto para as letras.

Também não vejo necessidade de reduce quando um loop simples resolve. Claro que para poucas validações tanto faz, mas reduce tem um overhead que pode fazer diferença caso precise rodar milhões de casos. Sem contar que - pelo menos na minha opinião - um loop simples deixa o código mais claro (mas aí admito que já é questão de gosto).

Outro ponto é que no cálculo do segundo dígito verificador vc converte dv1 para string, somente para obter o seu valor numérico de novo. É uma volta desnecessária (e entendo que só foi preciso fazer isso para poder usar dentro do reduce, pois o array deve ser de caracteres).

Por fim, podemos manter apenas um array de pesos, já que somente a primeira posição é diferente (o cálculo do primeiro dígito não usa a primeira posição, e o do segundo dígito usa todas). Ficaria assim:

function dv(sum) {
    const resto = sum % 11;
    return resto < 2 ? 0 : 11 - resto;
}

function calcDv(cnpjBase: string): [number, number] {
    const weights = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    var sum1 = 0, sum2 = 0;
    // calcula as duas somas simultaneamente, no mesmo loop
    // o que muda é o peso usado por cada uma
    for (let i = 0; i < cnpjBase.length; i++) {
        const val = cnpjBase.charCodeAt(i) - 48;
        sum1 += val * weights[i + 1];
        sum2 += val * weights[i];
    }

    const dv1 = dv(sum1);
    // completa sum2 com o peso que faltava, e já retorna os dígitos
    return [dv1, dv(sum2 + dv1 * 2)];
}

console.log(calcDv('12ABC34501DE')); // [3, 5]

E também coloquei ponto-e-vírgula no final das linhas. Pode parecer "frescura", e sei que o TypeScript (e também o JavaScript) "aceita" o código sem ponto e vírgula e "funciona", mas isso evita algumas situações bizarras que podem ocorrer se você não usá-los, como essa e essa (veja mais sobre isso aqui).


Só por curiosidade, fiz um teste usando o Benchmark.js para comparar as duas soluções. O resultado foi:

sem reduce x 36,216,689 ops/sec ±0.92% (92 runs sampled)
com reduce x 1,554,090 ops/sec ±0.65% (96 runs sampled)
Fastest is sem reduce

Os números acima são de operações por segundo, ou seja, quanto maior, melhor. Repare que se não usar reduce (como eu fiz) ele executou 36 milhões de operações por segundo, enquanto com reduce (mais o split e transformações de/para string, regex para ver se é dígito, etc) faz apenas 1 milhão e meio. Ou seja, essa simples alteração melhorou o desempenho em 23 vezes.

Também fiz esse mesmo teste no jsbench.me e o resultado foi similar: o código sem reduce fez 18 milhões de operações por segundo, enquanto com reduce fez 1,6 milhões.

Novamente, claro que se for para validar poucos CNPJ's (ou até se for centenas, ou poucos milhares), a diferença será insignificante. Mas se for usar para validar bases maiores (milhares ou milhões de registros, por exemplo), pode fazer diferença.

1

A validação ficou muito boa.

Um detalhe que percebi, é que na função toValue não precisa fazer o teste pra retornar parseInt se for número. Pode sempre fazer a conta do c.charCodeAt(0) - 48, pois os números ficarão com o valor correto. Do ponto de vista performance não sei se influencia em algo.

E vale lembrar que as letras precisam estar em maiúscula, pois o ASCII das minúsculas dariam conta errada. Mas aí, se a pessoa tentou validar com minúscula, tem que dar erro mesmo.

1

Vlw , quaquer coisa abre uma issue la, eu fiz pensando mais em usar apenas para min nos meus projetos pessoais, mas acabei ficando empolgado, to pensando em ampliar bastante

1